Python for PHP Developers: Deep Dive Part 3


What is it like moving from PHP to Python? Part 3 explores Python’s conditional operators and loops.

Using the broad overview from learningpython.org, we’ll examine the ins and outs of Python from a PHP developer’s perspective, while showing the equivalent PHP and Python code side-by-side.

Clone github.com/bill-c-martin/python-for-php-devs to get a Python environment setup and to follow along with the code examples in this series.

In Part 3 of this series, we will examine these in more detail to see how they compare in Python vs PHP:

Table of Contents

Conditions

Compared to PHP, Python conditions refreshingly read more like pseudocode, with some minor bash (like the elif) and SQL (like the and, or, is, is not) vibes.

Check it out:

admins = ["john", "sue", "bob"]
name = "john"
age = 23
admin = True

if name == "john" and age == 23:
    print('Your name is john')
    print('And you are 23 years old')

elif name == "john" or name == "rick":
    print("you are either john or rick..")

elif name in admins:
    print("you are an admin")

elif admin is not True:
    print("you are not an admin")

else:
    pass

It’s simple and intuitive enough to read, but there’s a lot to unpack. Let’s get to it. Starting with the blocks themselves.

Blocks

Instead of the usual { and } curly braces to define a block, Python uses a single : colon instead.

And since there’s no } closing that block in Python, the indentation matters.

PHP

PHP does actually allow you to omit curly braces for a 1-statement block. But nobody really does that, because it increases the likelihood of errors when new lines are added to the body.

// Allowed, technically speaking..
if ($name == "john")
    echo 'Your name is john';

if ($name == "john") {
    echo 'Your name is john';
    echo 'I am certain';
}

Python

Python, however, does consistently use the : to denote a block, regardless if it contains 1 or more statements:

if name == "john":
    print('Your name is john')

if name == "john":
    print('Your name is john')
    print('I am certain')

Think of it this way. If the body is not indented, Python thinks the if condition has no corresponding block (and throws an error):

if name == "john":
print('Your name is john')  # throws an error

If you have 2 statements, but don’t indent the last one, that last one basically “closes” the prior if statement and runs outside of the block:

if name == "john":
    print('Your name is john') # Runs only when name is John
print('You must be human.')    # Runs always

So, mind the indentation.

Nested If

Python has nested conditions too. These work exactly how you’d expect them to.

Again, mind the indentation, though.

PHP

if ($name == "john") {
    echo 'Your name is john';
    if($age == 23) {
        echo "And you are 23 years old";
    }
}

Python

if name == "john":
    print('Your name is john')
    if age == 23:
        print('And you are 23 years old')

If, Else If, Else Ladder

These also are exactly what you’d expect.

Except instead of PHP’s elseif, Python’s “Else If” is just elif (like bash 😉)

PHP

if ($name == "john") {
    echo 'Your name is john';
} elseif($name == "Bob") {
    echo 'Your name is Bob';
}
else {
    echo 'you are no one';
}

Python

if name == "john":
    print('Your name is john')
elif name == "Bob":
    print('Your name is Bob')
else:
    print('you are no one')

Ternary Operators (aka Conditional Expressions in Python)

Python calls ternary operators: “Conditional expressions”.

Same idea, different syntactic sugar.

Python’s is more readable, especially with the “then” value up front like that.

PHP

echo "Your name is: ";
echo ($name == "John") ? "John" : "unknown";

Python

print("Your name is: ")
print( "John" if name == "John" else "unknown" )

pass Statement

pass is a Python-only quirk. It is a null statement that does.. nothing at all.

It’s used as a placeholder inside the empty bodies of conditions (and loops, functions, and classes).

Why? Since bodies aren’t wrapped in curly braces in Python, and indentation dictates when the body begins and ends, then there is no body, and where there is no body, there is an error.

PHP

if ($name == "john") {
    // Works just fine in PHP
}
else {
    echo 'You are not John');
}

Python

if name == "john":
    # Throws an error in Python
else:
    print('You are not John')

It’s kind of like leaving the semicolon off of lines of PHP code. The parser runs into the next line, confused.

Operators

Logical Operators

PHP’s &&, ||, and ! is Python’s and, or, and not.

PHP

$x = 9.99;
echo ($x > 9 && $x < 10);  // true
echo ($x < 10 || $x > 10); // true
echo !($x > 9 && $x < 10); // false

Python

x = 9.99
print(x > 9 and x < 10)          # True
print(x < 10 or x > 10)          # True
print( not (x > 9 and x < 10) )  # False

Comparison Operators

PHP and Python use the same comparison operators: ==, !=, >, <, >=, and <=

However, Python does not have ===.

In fact, Python’s == confusingly falls somewhere in between PHP’s == and ===.

PHP

As we know, == is super duck-typey, and === isn’t:

var_dump(1 == '1');   // true
var_dump(1 == 1.0);   // true
var_dump(1 == true);  // true

var_dump(1 === '1');  // false
var_dump(1 === 1.0);  // false
var_dump(1 === true); // false

Python

Meanwhile, Python’s == is like PHP’s ==, except when it comes to strings:

print(1 == '1')  # False
print(1 == 1.0)  # True
print(1 == True) # True

This is going to throw you off a lot.

So why does that integer vs boolean comparison work in Python, but the integer vs string comparison doesn’t?

Because bool is a subclass of int, as will be demonstrated shortly in the “Type Checking” section, below.

Identity Operators

Python has two identity operators: is and is not. These test for reference equality - when two variables point to the same location in memory.

Do not confuse these with == and !=, which test for value equality - when two variables have the same value.

Coming from PHP, these are strangely easy to confuse even though PHP has no is or is not operators.

I suspect this is due to Python’s and, or, and not SQL-esque logical operators, which leads to our muscle memory of also using the is and is not also-SQL-esque identity operators within the same conditions.

This would be an insidious habit that needs breaking, because it could lead to subtle bugs.

Python

Alright, let’s check out is and is not vs == and !=.

a = [1,2,3]
b = [1,2,3]
print(a == b) # Prints out True. Same values.
print(a is b) # Prints out False. Different memory addresses.

x = [1,2,3]
y = x
print(x == y) # Prints out True. Same values.
print(x is y) # Prints out True too. Same memory address.

This is pretty straightforward.

Where it gets insidious is with ints, strings, floats etc:

x = 1
y = 1
print(x == y) # Prints out True. Same values.
print(x is y) # Prints out.. True.

# Print out.. same memory addresses!
print(id(x), id(y)) # Eg. 9788960 and 9788960

So what happened, how can x and y have the same memory address? Turns out, Python can cache integers and such into singleton objects for performance reasons.

Worse, CPython does not do this.

No, “CPython” was not a typo. CPython is the most common interpreter of Python “bytecode”. How do you know what interpreter you are using? Run these three in your terminal:

python3
import platform
platform.python_implementation()

It’ll probably output 'CPython'.

The problem is that your terminal, container, server, or some other environment might be running some other Python interpreter. So don’t confuse is and ==.

Luckily, Python 3.8 and onward will actually helpfully throw a SyntaxWarning if you even use is or is not to compare with literal values:

x = 1
print(x is 1) # SyntaxWarning: "is" with a literal. Did you mean "=="?

PHP

Surprise! PHP has no concept of reference equality. Which is why you will confuse is and ==. 😉

Membership Operators

Python’s in and not in operators check for values in iterable objects like strings, lists, dictionaries, etc.

Python

Pretty straightforward and intuitive:

x = [1,2,3]
y = "Hello world"

if 1 in x:
    print('1 is in [1,2,3]')
if 6 not in x:
    print('6 is not in [1,2,3]')
if "Hello" in y:
    print('"Hello" is in "Hello World"')
if "H" in y:
    print('And so is "H"')
if "X" not in y:
    print('But "X" is not')

PHP

Of course, in PHP, the same is achieved through its inconsistent, built-in functions:

Or for versions older than PHP8, you’re stuck with strpos() instead of str_contains(), which is a wildly different developer experience with the whole !== false thing. 😭

$x = [1,2,3];
$y = "Hello world";

if (in_array(1, $x)) {
    echo '1 is in [1,2,3]';
}
if (!in_array(6, $x)) {
    echo '6 is in [1,2,3]';
}
if (strpos($y, 'Hello') !== false) {
    echo '"Hello" is in "Hello World"';
}
if (strpos($y, 'H') !== false) {
    echo 'And so is "H"';
}
if (strpos($y, 'X') === false) {
    echo 'But "X" is not';
}

Bitwise Operators

The good news is that the “bitwise” operators for twos-compliment binary are the same in both PHP and Python: <<, >>, &, |, ~, and ^.

Type Checking

Python has very straightforward type checking compared to PHP.

Determining Type

Python

Python uses type() to determine the type for anything. The quirk is that you technically have to append the __name__ attribute to it to get the type in plain text:

print(type(123).__name__)                      # prints: int
print(type(3.14).__name__)                     # prints: float
print(type("Hello").__name__)                  # prints: str
print(type(["John", "Jane"]).__name__)         # prints: list
print(type({"John": 25, "Jane": 21}).__name__) # prints: dict

Same for objects:

class Employee:
  pass
john = Employee()

print(type(john).__name__) # prints: Employee

Without __name__, you’ll get something like this:

print(type(123))  # prints: <class 'int'>
print(type(john)) # prints: <class '__main__.Employee'>

Now you’re probably wondering why type() would return something like <class 'int'> in the first place?

That’ll be addressed in the next section.

PHP

PHP’s gettype() is mostly similar to Python’s type(), except it returns string representations by default:

echo gettype(123);              // prints: integer
echo gettype(3.14);             // prints: float
echo gettype("Hello");          // prints: string
echo gettype(["John", "Jane"]); // prints: array

And worse, it merely returns object for any and all objects:

class Employee{};
$john = new Employee();

echo gettype($john);  // prints: object

So of course you’ll need to use a different function for objects:

class Employee{};
$john = new Employee();

echo get_class($john); // prints: Employee

Which, in standard PHP fashion, gettype() and get_class() have inconsistent naming conventions.

Type Comparison

Python

Alright, so back to why type() returns stuff like this in the first place:

print(type(123))  # prints: <class 'int'>
print(type(john)) # prints: <class '__main__.Employee'>

Since you’re generally using type() for sake of type comparison, there’s no need to deal with strings at all. You can compare a type directly against a class:

print(type(123) == int)            # True
print(type(123).__name__ == 'int') # True

Same goes for custom classes too:

class Employee:
  pass
john = Employee()

print(type(john) == Employee)            # True
print(type(john).__name__ == 'Employee') # True

Type comparison in Python has one caveat though:

Consider the following:

# Parent class
class Employee:
  pass

# Child class Engineer, inherits parent class Employee:
class Engineer(Employee):
  pass

print(type(Employee()) is Employee) # True
print(type(Employee()) is Engineer) # False
print(type(Engineer()) is Employee) # False - except an Engineer IS an Employee..
print(type(Engineer()) is Engineer) # True

print(isinstance(Employee(), Employee)) # True
print(isinstance(Employee(), Engineer)) # False
print(isinstance(Engineer(), Engineer)) # True
print(isinstance(Engineer(), Employee)) # True

isinstance() works for primitives too:

print(isinstance(123, int))                       # True
print(isinstance(3.14, float))                    # True
print(isinstance("Hello", str))                   # True
print(isinstance(["John", "Jane"], list))         # True
print(isinstance({"John": 25, "Jane": 21}, dict)) # True

So for type comparison, you will generally want to go with isinstance().

PHP

Although PHP’s instanceof operator is somewhat similar to Python’s isinstance(), it is much more limited. And so you typically see all these one-off methods for type checking in PHP that you don’t have to deal with in Python:

echo is_int(123);                // True
echo is_float(3.14);             // True
echo is_string("John");          // True
echo is_array(["John", "Jane"]); // True

class Employee{};
$john = new Employee();

echo ($john instanceof Employee); // True

In summary, here we have a whole pile of methods in PHP for type checking and comparison, including a special instanceof operator, inconsistently named get_class() and gettype() methods, and the whole pile of one-off methods above.

Whereas with Python, it’s just type() and isinstance(), but really, just mostly isinstance().

Loops

Looping constructs are similar between PHP and Python, but also kind of very different at the same time.

for

Python does not have PHP’s C-style for loops that look like: for(i=0; i<10; i++).

What it does have is PHP’s foreach concept of iterating over.. iterables, which looks like: for x in y.

And to replace all that snarly, antiquated i=0; i<10; i++ stuff missing in Python, you use the range() method, or in this case: range(10).

Furthermore, Python’s for x in y looping construct supports all iterables, (or what Python calls “sequences”): lists, tuples, dictionaries, sets, strings, and ranges.

Python

The end result is consistency and readability:

# Iterate 10x
for i in range(10):
    print(i)

# Print some odd numbers
for i in range(1, 11, 2):
    print(i)

# Iterate each character in a string
for letter in "hello world":
    print(letter)

# Iterate over a list, etc
for name in ["John", "Jane", "Bob"]:
    print(name)

# Print first & last names
person = {"first": "Jane", "last": "Doe"}
for key in person:
    print(person[key])

PHP

Whereas with PHP, you have the cobbled-together-over-time vibe we know and love:

// Iterate 10x
for ($i = 0; $i < 10; $i++) {
    echo $i;
}

// Print some odd numbers
for ($i = 1; $i <= 11; $i=$i+2) {
    echo $i;
}

# Iterate over an array, etc
foreach(["John", "Jane", "Bob"] as $name) {
    echo $name;
}

# Print first & last names
foreach(["first" => "Jane", "last" => "Doe"] as $name) {
    echo $name;
}

while

While loops are basically the same in Python as PHP, with one caveat worth pointing out: Python does not have do-while loops.

However, these can still be implemented with an “infinite” loop (while True), followed by a break when the condition is met.

Python

count = 0

# Print 0 through 4
while count < 5:
    print(count)
    count += 1

# Print 5 through 9
while True:
    print(count)
    count += 1
    
    if count == 10:
        break

PHP

$count = 0;

// Print 0 through 4
while ($count < 5) {
    echo $count;
    $count++;
}

// Print 5 through 9
do {
    echo $count;
    $count++;
} while($count < 10);

for else and while else

The usage of else with for and while loops is something interesting that Python has that PHP does not.

It has to be paired with a break to make sense to even use. Otherwise, the else will always fire if a for or while loop is not “broken” with break.

So, why do this? If the loop exhausts itself without doing something, it gives you the opportunity to intercept.

However, this is just syntactic sugar that saves you from having to declare a flag and separate if condition, and so this behavior can be easily replicated in PHP.

Python

Here is an example of the else firing:

for name in ["John", "Jane", "Bob"]:
    if name == "Joe":
        print("Joe was found")
        break
else:
    # This fires because the above for loop did not break. Joe was never found.
    raise ValueError("Joe not found")

And an example of the else not firing:

for name in ["John", "Jane", "Bob"]:
    if name == "Bob":
        print("Bob was found")
        # However since Bob is found, the for loop is exited (not exhausted)
        break
else:
    # And so this never fires, as a result
    raise ValueError("Joe not found")

PHP

Since PHP does not have the built-in else construct for foreach and while loops, the code is a bit more verbose (and error prone) due to the boolean flag and additional if conditions:

$found = false;

foreach(["John", "Jane", "Bob"] as $name){
    if ($name == "Joe") {
        $found = true;
        print("Joe was found");
        break;
    }
}

if(!$found) {
    # This fires because Joe was never found, the flag was never set to true.
    throw new Exception("Joe not found");
}
$found = false;

foreach(["John", "Jane", "Bob"] as $name){
    if ($name == "Bob") {
        $found = true;
        print("Bob was found");
        break;
    }
}

if(!$found) {
    # Since Bob was found, and the flag was set to true, this never fires
    throw new Exception("Bob not found");
}

map and filter

Maps and filters are nearly identical in Python and PHP.

Python

However, in Python, there are two caveats.

First, they return map and filter objects, not the data type you passed into them. This is different than PHP where you pass an array in, and get an array out of it. In the example below, their output is casted back to a list using list().

Second, they use lambdas. A lamba in Python is just an anonymous function, but the syntax looks more like ES6 arrow functions. Other than that they work identically to PHP’s closures, which are also just anonymous functions.

values = [1, 2, 3, 4, 5]

doubled_values = list(map(lambda x: x * 2, values))
odd_values = list(filter(lambda x: x % 2, values))

print(doubled_values) # Prints: [2, 4, 6, 8, 10]
print(odd_values)     # Prints: [1, 3, 5]

PHP

And of course, with PHP, you have the inconsistent parameter ordering of array_map and array_filter..

$values = [1, 2, 3, 4, 5];

$doubled_values = array_map(function ($x) {
    return $x * 2;
}, $values);

$odd_values = array_filter($values, function ($x) {
    return $x % 2;
});

print_r($doubled_values);
print_r($odd_values);

Conclusion

Compared to PHP, Python:

In Part 4, we will finally dive into classes and functions, go deeper into lambdas, and also dive into closures (yes, Python has these, and they are true closures), while comparing these to PHP-equivalent code every step of the way.