PONY λ M2 Modula-2

PHP.CodeCompared.To/Python

An interactive executable cheatsheet comparing PHP and Python

PHP 8.3.11 (Judge0) or PHP 8.5.5 (WASM) Python 3.13
Output & Running
Hello, World
<?php echo "Hello, World!\n";
print("Hello, World!")
There is no <?php opening tag, no $ on variables, and no trailing semicolon. Python's print() is a function that adds a newline for you, so you do not append \n the way you would with echo.
Running a Script
<?php // PHP is interpreted. Run the file directly: // php script.php $name = "World"; echo "Hello, {$name}!\n";
# Python is interpreted too. Run the file directly: # python3 script.py name = "World" print(f"Hello, {name}!")
Both languages are interpreted and need no compile step. The biggest visual difference is that a Python file is all code — there is no <?php ... ?> island inside text, because Python is not a templating language. Comments use #, which PHP also accepts.
Braces vs Indentation
<?php $temperature = 30; if ($temperature > 25) { echo "Warm\n"; } else { echo "Cool\n"; }
temperature = 30 if temperature > 25: print("Warm") else: print("Cool")
Python has no curly braces. A colon opens a block and indentation (four spaces by convention) defines its body — the layout you would write anyway in PHP is the actual syntax here. Mixing tabs and spaces is an error, so pick one.
Comments & Docstrings
<?php // a single-line comment # also a single-line comment /* a block comment */ /** * A docblock, read by IDEs and phpDocumentor. */ function area(float $radius): float { return 3.14159 * $radius ** 2; } echo area(2), "\n";
# the only single-line comment style def area(radius: float) -> float: """Return the area of a circle (a real docstring).""" return 3.14159 * radius ** 2 print(area(2)) print(area.__doc__)
Python has just one comment marker, #. There is no block-comment syntax; instead a string literal as the first statement of a function, class, or module is its docstring, readable at runtime via __doc__ and by tooling — a step beyond PHP's docblocks, which are only comments.
Variables & Types
Declaring Variables
<?php $count = 42; $price = 9.99; $name = "Alice"; $active = true; $nothing = null; echo "{$name} {$count} {$price} ", var_export($active, true), "\n";
count = 42 price = 9.99 name = "Alice" active = True nothing = None print(name, count, price, active)
Variables carry no $ sigil — they look like bare words. The booleans are capitalised (True / False) and the empty value is None, of which there is exactly one (PHP's null has the same role). Assignment creates the variable; there is no declaration keyword.
Checking Types
<?php $value = 42; echo gettype($value), "\n"; // integer echo is_int($value) ? "int\n" : "no\n"; echo get_debug_type(3.14), "\n"; // float
value = 42 print(type(value).__name__) # int print(isinstance(value, int)) # True print(type(3.14).__name__) # float
Python's type() returns the class object; type(x).__name__ gives a readable name like PHP's get_debug_type(). Use isinstance() rather than comparing types directly, because it also accepts subclasses — the counterpart to PHP's instanceof.
Type Hints
<?php function greet(string $name, int $times = 1): string { return str_repeat("Hi {$name}! ", $times); } echo greet("Sam", 2), "\n";
def greet(name: str, times: int = 1) -> str: return f"Hi {name}! " * times print(greet("Sam", 2))
Both languages bolt gradual typing onto a dynamic core, but with a key difference: PHP's type declarations are enforced at runtime and can raise a TypeError, whereas Python's annotations are not checked at runtime at all — they are hints for tools like mypy and your editor. A wrong type passes silently unless a checker catches it.
Numbers & Division
<?php echo intdiv(7, 2), "\n"; // 3 echo 7 / 2, "\n"; // 3.5 echo 7 % 2, "\n"; // 1 echo 2 ** 10, "\n"; // 1024 echo PHP_INT_MAX, "\n";
print(7 // 2) # 3 (floor division) print(7 / 2) # 3.5 (always float) print(7 % 2) # 1 print(2 ** 10) # 1024 print(10 ** 100) # integers never overflow
In Python / always produces a float (so 4 / 2 is 2.0), and // is integer floor division — the reverse of PHP, where / can yield an int. Python integers are arbitrary precision: there is no PHP_INT_MAX ceiling and no silent promotion to float on overflow.
Truthiness
<?php foreach ([0, "", "0", [], null, "false"] as $value) { echo $value || true ? "" : ""; // (just iterating) echo ($value ? "truthy" : "falsy"), " "; } echo "\n"; // falsy falsy falsy falsy falsy truthy
for value in [0, "", [], {}, None, "false"]: print("truthy" if value else "falsy", end=" ") print() # falsy falsy falsy falsy falsy truthy
Empty collections ([], {}) are falsy in both languages. The notable trap for a PHP developer: the string "0" is truthy in Python but falsy in PHP. Python falsy values are None, False, zero, and every empty container — and nothing else.
Strings
Interpolation
<?php $name = "Alice"; $age = 30; echo "{$name} is {$age}\n"; echo $name . " is " . $age . "\n";
name = "Alice" age = 30 print(f"{name} is {age}") print(name + " is " + str(age))
Python's f-strings are the everyday tool, and like PHP they embed expressions directly: f"{name}". The big difference is concatenation — Python's + will not coerce a number to a string, so you must call str(age) explicitly, unlike PHP's . operator which converts automatically.
Formatting Numbers
<?php $price = 1234.5; echo "$" . number_format($price, 2), "\n"; // $1,234.50 printf("%5d\n", 42); // 42
price = 1234.5 print(f"${price:,.2f}") # $1,234.50 print(f"{42:5d}") # 42
Python folds formatting directly into f-strings with a : mini-language: {price:,.2f} means thousands separators and two decimals. It covers the same ground as sprintf / printf but inline at the interpolation site rather than in a separate format string.
Common Methods
<?php $text = " Hello, World "; echo strtoupper(trim($text)), "\n"; // HELLO, WORLD echo str_replace("World", "PHP", "Hello World"), "\n"; echo strpos("Hello", "ell"), "\n"; // 1 echo str_contains("Hello", "ell") ? "yes\n" : "no\n";
text = " Hello, World " print(text.strip().upper()) # HELLO, WORLD print("Hello World".replace("World", "Python")) print("Hello".find("ell")) # 1 print("ell" in "Hello") # True
Strings are objects with methods in Python, so you chain text.strip().upper() instead of nesting strtoupper(trim($text)). Substring testing is the readable "ell" in "Hello" rather than str_contains, and the same in works for lists and dicts too.
Split & Join
<?php $csv = "a,b,c"; $parts = explode(",", $csv); print_r($parts); echo implode("-", $parts), "\n"; // a-b-c
csv = "a,b,c" parts = csv.split(",") print(parts) # ['a', 'b', 'c'] print("-".join(parts)) # a-b-c
The pair is split / join rather than explode / implode. Note that join is a string method called on the separator ("-".join(parts)), which surprises most newcomers — the separator, not the list, owns the operation.
Slicing & Indexing
<?php $text = "Python"; echo $text[0], "\n"; // P echo substr($text, 0, 3), "\n"; // Pyt echo substr($text, -2), "\n"; // on echo strrev($text), "\n"; // nohtyP
text = "Python" print(text[0]) # P print(text[0:3]) # Pyt print(text[-2:]) # on print(text[::-1]) # nohtyP (reverse via step)
Python's slice syntax text[start:stop:step] replaces substr and a pile of other helpers. Negative indices count from the end (text[-2:]), and a step of -1 reverses — there is no separate strrev. The same slicing works on lists.
Arrays vs Lists & Dicts
One Array vs Four Types
<?php // PHP has ONE array type that is both list and map: $list = [1, 2, 3]; $map = ["name" => "Alice", "age" => 30]; echo $list[0], " ", $map["name"], "\n";
# Python splits this into distinct types: ordered_list = [1, 2, 3] # list person = {"name": "Alice", "age": 30} # dict coordinate = (10, 20) # tuple (immutable) unique = {1, 2, 3} # set print(ordered_list[0], person["name"])
This is the central difference. PHP's single ordered-map array fills every role; Python splits it into list (ordered, mutable), dict (key-value), tuple (immutable sequence), and set (unique, unordered). Choosing the right one is part of writing idiomatic Python.
List Operations
<?php $numbers = [3, 1, 2]; $numbers[] = 4; // append sort($numbers); echo count($numbers), "\n"; // 4 echo in_array(2, $numbers) ? "yes\n" : "no\n"; print_r(array_slice($numbers, 1, 2));
numbers = [3, 1, 2] numbers.append(4) numbers.sort() print(len(numbers)) # 4 print(2 in numbers) # True print(numbers[1:3]) # [2, 3]
Lists are objects, so you call numbers.append(4) and numbers.sort() as methods rather than passing the array to array_push / sort. Length is the universal len() function, and membership is the same in keyword used for strings — no in_array.
Dict Operations
<?php $person = ["name" => "Alice"]; $person["age"] = 30; echo isset($person["name"]) ? "yes\n" : "no\n"; echo $person["email"] ?? "none", "\n"; // none foreach ($person as $key => $value) { echo "{$key}={$value} "; } echo "\n";
person = {"name": "Alice"} person["age"] = 30 print("name" in person) # True print(person.get("email", "none")) # none for key, value in person.items(): print(f"{key}={value}", end=" ") print()
Key existence is "name" in person (the in keyword again, replacing isset), and the safe lookup with a default is person.get(key, default) — the counterpart to PHP's ??. Iterating key/value pairs uses .items() rather than foreach ... as $key => $value.
Tuples & Unpacking
<?php // PHP uses list() / [] destructuring on arrays: [$x, $y] = [10, 20]; echo "{$x},{$y}\n"; // 10,20 [$x, $y] = [$y, $x]; // swap echo "{$x},{$y}\n"; // 20,10
point = (10, 20) # an immutable tuple x, y = point # unpacking print(f"{x},{y}") # 10,20 x, y = y, x # swap, no temp variable print(f"{x},{y}") # 20,10
Tuples are immutable sequences with no direct PHP equivalent — use them for fixed-size groupings like a coordinate or a returned pair. Unpacking (x, y = point) generalises PHP's array destructuring and makes the swap idiom x, y = y, x read naturally without a temporary.
Sets
<?php // PHP has no set type; the idiom is array keys: $first = array_flip([1, 2, 3]); $second = array_flip([2, 3, 4]); $common = array_intersect_key($first, $second); print_r(array_keys($common)); // 2, 3
first = {1, 2, 3} second = {2, 3, 4} print(first & second) # {2, 3} intersection print(first | second) # {1, 2, 3, 4} union print(first - second) # {1} difference
Python has a real set type with operator algebra: & for intersection, | for union, - for difference. PHP has no set, so the usual workaround is to abuse array keys with array_flip and the array_*_key functions — sets make this both clearer and faster.
Map & Filter
<?php $numbers = [1, 2, 3, 4, 5]; $doubled = array_map(fn($n) => $n * 2, $numbers); $evens = array_filter($numbers, fn($n) => $n % 2 === 0); $total = array_sum($numbers); print_r($doubled); print_r(array_values($evens)); echo $total, "\n";
numbers = [1, 2, 3, 4, 5] doubled = [n * 2 for n in numbers] evens = [n for n in numbers if n % 2 == 0] total = sum(numbers) print(doubled) # [2, 4, 6, 8, 10] print(evens) # [2, 4] print(total) # 15
Python prefers comprehensions over array_map / array_filter — a single readable expression replaces both. Note array_filter preserves PHP keys (hence the array_values reindex), whereas a Python comprehension always returns a fresh, contiguous list.
Control Flow
Conditionals
<?php $score = 85; if ($score >= 90) { echo "A\n"; } elseif ($score >= 80) { echo "B\n"; } else { echo "C\n"; }
score = 85 if score >= 90: print("A") elif score >= 80: print("B") else: print("C")
The keyword is elif, not elseif or else if. Conditions need no parentheses, and the chaining comparison 80 <= score < 90 is valid Python — a small expressiveness win PHP lacks.
Ternary & Null Handling
<?php $age = 20; $label = $age >= 18 ? "adult" : "minor"; echo $label, "\n"; $name = null; echo $name ?? "anonymous", "\n"; // anonymous
age = 20 label = "adult" if age >= 18 else "minor" print(label) name = None print(name or "anonymous") # anonymous
Python's conditional expression reads in value-first order: VALUE if CONDITION else OTHER. There is no dedicated ?? operator; for a default-when-falsy the idiom is name or "anonymous" (note this also fires on empty strings and zero, so it is closer to PHP's ?: than to ??).
Loops & Ranges
<?php for ($index = 0; $index < 3; $index++) { echo $index, " "; } echo "\n"; foreach (["a", "b", "c"] as $letter) { echo $letter, " "; } echo "\n";
for index in range(3): print(index, end=" ") print() for letter in ["a", "b", "c"]: print(letter, end=" ") print()
Python has no C-style three-part for loop; you iterate over a sequence, and counting comes from range(3) which yields 0, 1, 2. The foreach-over-values form maps directly to for letter in [...].
Index While Iterating
<?php $fruits = ["apple", "banana", "cherry"]; foreach ($fruits as $index => $fruit) { echo "{$index}: {$fruit}\n"; }
fruits = ["apple", "banana", "cherry"] for index, fruit in enumerate(fruits): print(f"{index}: {fruit}")
When you need both the position and the value of a list, wrap it in enumerate() rather than maintaining a counter. It is the direct equivalent of PHP's foreach ($fruits as $index => $fruit) for a plain list, and you can pass start=1 to begin at one.
Iterating Two Lists
<?php $names = ["Alice", "Bob"]; $scores = [90, 85]; foreach ($names as $index => $name) { echo "{$name}: {$scores[$index]}\n"; }
names = ["Alice", "Bob"] scores = [90, 85] for name, score in zip(names, scores): print(f"{name}: {score}")
To walk two (or more) lists in lockstep, zip() pairs them element by element, so you never index a second array by a shared counter. It stops at the shortest input, which is usually what you want.
Comprehensions
List Comprehensions
<?php $squares = []; foreach (range(1, 5) as $number) { $squares[] = $number ** 2; } print_r($squares); // 1, 4, 9, 16, 25
squares = [number ** 2 for number in range(1, 6)] print(squares) # [1, 4, 9, 16, 25]
The list comprehension is Python's signature feature and has no PHP equivalent. It collapses the build-an-empty-array-then-loop pattern into one expression. Note range(1, 6) is half-open — it stops before 6, unlike PHP's inclusive range(1, 5).
Dict & Set Comprehensions
<?php $words = ["apple", "banana", "cherry"]; $lengths = []; foreach ($words as $word) { $lengths[$word] = strlen($word); } print_r($lengths);
words = ["apple", "banana", "cherry"] lengths = {word: len(word) for word in words} print(lengths) # {'apple': 5, 'banana': 6, 'cherry': 6} initials = {word[0] for word in words} # a set print(sorted(initials)) # ['a', 'b', 'c']
The same comprehension syntax builds dicts ({key: value for ...}) and sets ({value for ...}), not just lists. This single, uniform construct replaces a whole family of PHP array-building loops and keeps the transformation logic in one place.
Generators
<?php function countUp(int $limit) { for ($number = 1; $number <= $limit; $number++) { yield $number; } } foreach (countUp(3) as $value) { echo $value, " "; } echo "\n";
def count_up(limit): for number in range(1, limit + 1): yield number for value in count_up(3): print(value, end=" ") print() # A generator expression — lazy, parenthesised: total = sum(square for square in (n * n for n in range(4))) print(total) # 14
Both languages have lazy generators via yield, and they behave almost identically. Python adds a parenthesised generator expression — like a list comprehension but lazy — which is ideal for feeding sum(), max(), or any() without building an intermediate list.
Functions
Defining Functions
<?php function add(int $first, int $second): int { return $first + $second; } echo add(2, 3), "\n"; // 5
def add(first: int, second: int) -> int: return first + second print(add(2, 3)) # 5
The keyword is def instead of function, the return type follows a -> arrow, and there are no braces. Functions defined at module level are global by default — there is no $ scoping subtlety, but see the gotcha about reassigning globals.
Default & Keyword Arguments
<?php function connect(string $host, int $port = 5432, bool $ssl = false): string { return "{$host}:{$port} ssl=" . ($ssl ? "1" : "0"); } // PHP 8 named arguments: echo connect("db.example.com", ssl: true), "\n";
def connect(host, port=5432, ssl=False): return f"{host}:{port} ssl={ssl}" # keyword arguments, like PHP 8 named args: print(connect("db.example.com", ssl=True))
Default parameters work the same way, and Python's keyword arguments are the model PHP 8's named arguments were based on. One sharp difference: a default value is evaluated once at definition time, so a mutable default like [] is shared across calls — see the gotchas section.
Variadic & Spread
<?php function total(...$numbers): int { return array_sum($numbers); } echo total(1, 2, 3), "\n"; // 6 $values = [4, 5, 6]; echo total(...$values), "\n"; // 15
def total(*numbers): return sum(numbers) print(total(1, 2, 3)) # 6 values = [4, 5, 6] print(total(*values)) # 15 options = {"sep": "-"} print("a", "b", **options) # a-b (** spreads a dict)
Python uses *numbers to collect positional arguments into a tuple (PHP's ...$numbers) and the same * to spread a list at the call site. The extra trick is **, which collects or spreads keyword arguments through a dict — PHP has no direct equivalent.
Lambdas & Closures
<?php $multiplier = 3; $scale = fn($value) => $value * $multiplier; // auto-captures echo $scale(5), "\n"; // 15 $numbers = [5, 2, 8, 1]; usort($numbers, fn($a, $b) => $a <=> $b); print_r($numbers);
multiplier = 3 scale = lambda value: value * multiplier # captures automatically print(scale(5)) # 15 numbers = [5, 2, 8, 1] numbers.sort(key=lambda value: value) print(numbers) # [1, 2, 5, 8]
Python's lambda is the counterpart to PHP's arrow function fn() =>, and both capture surrounding variables automatically (no PHP use (...) clause needed). A lambda is limited to a single expression; for anything longer, define a named function with def.
Returning Multiple Values
<?php function minMax(array $numbers): array { return [min($numbers), max($numbers)]; } [$low, $high] = minMax([3, 7, 1, 9]); echo "{$low} {$high}\n"; // 1 9
def min_max(numbers): return min(numbers), max(numbers) # a tuple low, high = min_max([3, 7, 1, 9]) print(low, high) # 1 9
Returning several values is idiomatic: a comma-separated return builds a tuple, which the caller unpacks. It is the same shape as PHP's "return an array, then destructure", but reads more directly because the packing and unpacking are implicit.
Classes & OOP
Defining a Class
<?php class Greeter { public function __construct(private string $name) {} public function greet(): string { return "Hello, {$this->name}!"; } } $greeter = new Greeter("Alice"); echo $greeter->greet(), "\n";
class Greeter: def __init__(self, name): self.name = name def greet(self): return f"Hello, {self.name}!" greeter = Greeter("Alice") print(greeter.greet())
The constructor is the dunder method __init__, and crucially self is an explicit first parameter of every method — Python never hides the receiver the way PHP's implicit $this does. Instantiation has no new keyword; you call the class like a function.
Attributes & Member Access
<?php class Counter { public int $count = 0; public function increment(): void { $this->count++; } } $counter = new Counter(); $counter->increment(); echo $counter->count, "\n"; // 1
class Counter: def __init__(self): self.count = 0 def increment(self): self.count += 1 counter = Counter() counter.increment() print(counter.count) # 1
Member access uses a dot (counter.count), not ->. Attributes are created by assigning to self.something inside __init__ rather than declared in a property list. There is no ++ operator, so increment is self.count += 1.
Inheritance
<?php class Animal { public function __construct(protected string $name) {} public function speak(): string { return "..."; } } class Dog extends Animal { public function speak(): string { return "{$this->name} says Woof"; } } echo (new Dog("Rex"))->speak(), "\n";
class Animal: def __init__(self, name): self.name = name def speak(self): return "..." class Dog(Animal): def speak(self): return f"{self.name} says Woof" print(Dog("Rex").speak())
A subclass lists its parent in parentheses: class Dog(Animal). There is no extends keyword, and overriding a method needs no annotation. To call the parent's version you use super().method(), much like PHP's parent::method().
Dataclasses vs Readonly Properties
<?php class Point { public function __construct( public readonly int $x, public readonly int $y, ) {} } $point = new Point(3, 4); echo "{$point->x},{$point->y}\n"; // 3,4
from dataclasses import dataclass @dataclass(frozen=True) class Point: x: int y: int point = Point(3, 4) print(f"{point.x},{point.y}") # 3,4 print(point) # Point(x=3, y=4)
The @dataclass decorator generates __init__, a readable __repr__, and equality from the annotated fields — close to PHP's constructor property promotion but with more for free. Passing frozen=True makes instances immutable, the rough counterpart to PHP 8.1's readonly properties.
Static & Class Methods
<?php class MathUtil { public static float $pi = 3.14159; public static function square(float $value): float { return $value * $value; } } echo MathUtil::square(5), "\n"; // 25 echo MathUtil::$pi, "\n";
class MathUtil: pi = 3.14159 # a class attribute @staticmethod def square(value): return value * value print(MathUtil.square(5)) # 25 print(MathUtil.pi)
Static methods are marked with the @staticmethod decorator and accessed with a plain dot (MathUtil.square(5)) — there is no :: scope-resolution operator. A variable assigned directly in the class body, like pi, is a shared class attribute.
Magic / Dunder Methods
<?php class Money { public function __construct(public int $cents) {} public function __toString(): string { return "$" . number_format($this->cents / 100, 2); } } $wallet = new Money(1299); echo $wallet, "\n"; // $12.99
class Money: def __init__(self, cents): self.cents = cents def __str__(self): return f"${self.cents / 100:.2f}" def __add__(self, other): return Money(self.cents + other.cents) wallet = Money(1299) + Money(1) print(wallet) # $13.00
Python's "dunder" methods are the analogue of PHP's magic methods: __str__ mirrors __toString, __init__ mirrors __construct. Python goes further by letting you overload operators — defining __add__ makes + work on your objects, which PHP does not allow.
Pattern Matching & Enums
Match
<?php $status = 404; $message = match (true) { $status < 300 => "ok", $status < 400 => "redirect", $status < 500 => "client error", default => "server error", }; echo $message, "\n"; // client error
status = 404 match status: case n if n < 300: message = "ok" case n if n < 400: message = "redirect" case n if n < 500: message = "client error" case _: message = "server error" print(message) # client error
Both languages spell the keyword match, but they differ in nature: PHP's match is an expression that returns a value and compares with ===, while Python's match is a statement for structural pattern matching. The case _ wildcard is Python's default, and if guards refine each case.
Structural Patterns
<?php // PHP's match cannot destructure; you check by hand: $point = ["x" => 0, "y" => 5]; if ($point["x"] === 0 && $point["y"] === 0) { echo "origin\n"; } elseif ($point["x"] === 0) { echo "on y-axis at {$point['y']}\n"; } else { echo "elsewhere\n"; }
point = {"x": 0, "y": 5} match point: case {"x": 0, "y": 0}: print("origin") case {"x": 0, "y": y}: print(f"on y-axis at {y}") case _: print("elsewhere")
This is where Python's match outclasses PHP's: it destructures as it matches, binding y from the dict in the same line that tests the shape. PHP's match only compares scalar values, so equivalent logic falls back to nested if checks.
Enums
<?php enum Suit: string { case Hearts = "hearts"; case Spades = "spades"; public function color(): string { return match ($this) { Suit::Hearts => "red", Suit::Spades => "black", }; } } echo Suit::Hearts->value, " ", Suit::Hearts->color(), "\n";
from enum import Enum class Suit(Enum): HEARTS = "hearts" SPADES = "spades" def color(self): return "red" if self is Suit.HEARTS else "black" print(Suit.HEARTS.value, Suit.HEARTS.color())
Python enums subclass Enum, and a backed value is given by assignment (HEARTS = "hearts") — read back with .value, exactly like PHP 8.1's backed enums. Methods work the same way, and members compare by identity with is.
Error Handling
Try / Except
<?php try { $result = 10 / 0; } catch (DivisionByZeroError $error) { echo "Caught: ", $error->getMessage(), "\n"; } finally { echo "Done\n"; }
try: result = 10 / 0 except ZeroDivisionError as error: print(f"Caught: {error}") finally: print("Done")
The keyword is except, not catch, and the bound variable comes after as. Printing the exception object directly gives its message, so there is no getMessage(). The finally clause behaves identically to PHP's.
Raising Exceptions
<?php function withdraw(int $balance, int $amount): int { if ($amount > $balance) { throw new InvalidArgumentException("Insufficient funds"); } return $balance - $amount; } try { withdraw(100, 200); } catch (InvalidArgumentException $error) { echo $error->getMessage(), "\n"; }
def withdraw(balance, amount): if amount > balance: raise ValueError("Insufficient funds") return balance - amount try: withdraw(100, 200) except ValueError as error: print(error)
You raise rather than throw, and Python ships a rich tree of built-in exception types — ValueError, KeyError, TypeError — that you reach for before writing your own. Custom exceptions subclass Exception, the parallel of extending PHP's Exception.
Context Managers vs finally
<?php // PHP relies on finally to guarantee cleanup: $handle = fopen("php://temp", "w+"); try { fwrite($handle, "data"); rewind($handle); echo stream_get_contents($handle), "\n"; } finally { fclose($handle); }
import io # 'with' guarantees cleanup — no explicit finally: with io.StringIO() as buffer: buffer.write("data") buffer.seek(0) print(buffer.read()) # buffer is automatically closed here
The with statement and its context managers are Python's preferred cleanup mechanism, replacing the manual try ... finally fclose() dance. On leaving the block the resource is released automatically, even on an exception — most notably with open(path) as file: for files.
Modules & Packages
Imports
<?php // PHP: require the file, then use names (often via Composer autoload): // require 'vendor/autoload.php'; echo str_pad("7", 3, "0", STR_PAD_LEFT), "\n"; // 007 echo strtoupper("hi"), "\n";
import math from datetime import date print(math.sqrt(16)) # 4.0 print(f"{7:03d}") # 007 print(date(2026, 6, 20).year) # 2026
Python loads code with import rather than require, and almost everything beyond the core builtins lives in a module you import first — even math. The from module import name form pulls a single name into scope, similar to a PHP use statement for a namespaced symbol.
Your Own Modules
<?php // geometry.php namespace Geometry; function area(float $radius): float { return 3.14159 * $radius ** 2; } // main.php // require 'geometry.php'; // echo \Geometry\area(2);
# geometry.py def area(radius): return 3.14159 * radius ** 2 # main.py import geometry print(geometry.area(2)) # or: from geometry import area
Every .py file is a module — its filename is the module name, with no namespace declaration needed. Importing geometry runs that file once and exposes its names under geometry.. This example spans two files, so it cannot run inline here.
The Main Guard
<?php // A PHP file runs top to bottom when required; the common // guard is checking whether it was invoked directly: if (PHP_SAPI === "cli" && realpath($argv[0]) === __FILE__) { echo "Running as a script\n"; }
def main(): print("Running as a script") if __name__ == "__main__": main()
The if __name__ == "__main__": idiom is everywhere in Python. A module's __name__ is "__main__" only when run directly, and its own name when imported — so this block lets a file act as both an importable library and a runnable script, like the PHP CLI-entry check.
Composer vs pip
<?php // composer.json declares dependencies; install with: // composer require guzzlehttp/guzzle // Composer writes to vendor/ and a composer.lock file. echo "Composer manages PHP packages\n";
# pip installs from PyPI into the active environment: # python3 -m pip install requests # A pyproject.toml or requirements.txt pins versions; # a virtual environment (python3 -m venv .venv) isolates them. print("pip manages Python packages")
PyPI is Python's Packagist and pip is its Composer. The big mindset shift is the virtual environment: rather than a per-project vendor/ directory, Python isolates dependencies in a venv you activate, keeping each project's packages separate from the system interpreter.
Standard Library
JSON
<?php $person = ["name" => "Alice", "age" => 30]; $encoded = json_encode($person); echo $encoded, "\n"; // {"name":"Alice","age":30} $decoded = json_decode($encoded, true); echo $decoded["name"], "\n"; // Alice
import json person = {"name": "Alice", "age": 30} encoded = json.dumps(person) print(encoded) # {"name": "Alice", "age": 30} decoded = json.loads(encoded) print(decoded["name"]) # Alice
The verbs are json.dumps (dump-string) to encode and json.loads (load-string) to decode — the s suffix distinguishes the string forms from dump / load, which work on file objects. Decoding gives a plain dict, so there is no associative-vs-object flag like PHP's second argument.
Sorting with a Key
<?php $people = [ ["name" => "Alice", "age" => 30], ["name" => "Bob", "age" => 25], ]; usort($people, fn($a, $b) => $a["age"] <=> $b["age"]); echo $people[0]["name"], "\n"; // Bob
people = [ {"name": "Alice", "age": 30}, {"name": "Bob", "age": 25}, ] people.sort(key=lambda person: person["age"]) print(people[0]["name"]) # Bob
Python sorts by a key functionkey=lambda person: person["age"] extracts the value to compare — rather than a two-argument comparator with the spaceship operator. This is both faster and clearer for the common "sort by one field" case. Use sorted(people, ...) for a new list instead of sorting in place.
Counting & Grouping
<?php $words = ["a", "b", "a", "c", "b", "a"]; $counts = array_count_values($words); arsort($counts); print_r($counts); // a=3, b=2, c=1
from collections import Counter words = ["a", "b", "a", "c", "b", "a"] counts = Counter(words) print(counts.most_common()) # [('a', 3), ('b', 2), ('c', 1)] print(counts["a"]) # 3
The collections module is a highlight of Python's standard library. Counter tallies items (like array_count_values) and adds .most_common() for free; defaultdict and deque live there too. Reaching for these specialised containers is very idiomatic.
Dates & Times
<?php $date = new DateTime("2026-06-20"); echo $date->format("Y-m-d"), "\n"; // 2026-06-20 $later = $date->modify("+1 week"); echo $later->format("Y-m-d"), "\n"; // 2026-06-27
from datetime import date, timedelta today = date(2026, 6, 20) print(today.isoformat()) # 2026-06-20 later = today + timedelta(weeks=1) print(later.isoformat()) # 2026-06-27
Python's datetime module models date arithmetic with timedelta objects you literally add: today + timedelta(weeks=1). It is more explicit than PHP's string-based modify("+1 week"), and .isoformat() gives the standard YYYY-MM-DD rendering without a format string.
⚠ Gotchas for PHP Devs
Mutable Default Arguments
<?php // PHP evaluates the default on every call — safe: function append(int $value, array $into = []): array { $into[] = $value; return $into; } print_r(append(1)); // [1] print_r(append(2)); // [2] — fresh each time
# Python evaluates the default ONCE — the list is shared! def append(value, into=None): if into is None: into = [] into.append(value) return into print(append(1)) # [1] print(append(2)) # [2] — correct, thanks to the None guard
This is the most infamous Python trap for newcomers. A default argument is evaluated a single time at function definition, so a mutable default like into=[] persists and accumulates across calls. The fix is the universal idiom: default to None and create the real value inside the function.
is vs ==
<?php // PHP: == is loose, === is strict (value + type): var_dump(1000 == 1000); // true var_dump("1" == 1); // true (loose!) var_dump("1" === 1); // false (strict)
# Python: == is value equality, 'is' is identity: print(1000 == 1000) # True (equal values) print("1" == 1) # False (no coercion ever) big = 1000 print(big is 1000) # False — never use 'is' for values
Python has no loose ==: it compares by value without type juggling, so "1" == 1 is simply False and you never need PHP's ===. The separate is operator checks object identity, not equality — reserve it for x is None, never for comparing numbers or strings.
Reassigning Globals
<?php $total = 0; function add(int $amount): void { global $total; // PHP needs 'global' to write $total += $amount; } add(5); echo $total, "\n"; // 5
total = 0 def add(amount): global total # Python needs 'global' to rebind total += amount add(5) print(total) # 5
Both languages require a global declaration to reassign a module-level variable from inside a function — a rare point of close agreement. The subtlety: in Python you can read a global without declaring it, and you can mutate a global list or dict in place; only rebinding the name needs global.
Assignment Copies References
<?php // PHP arrays are value types — assignment COPIES: $original = [1, 2, 3]; $copy = $original; $copy[] = 4; echo count($original), "\n"; // 3 — unchanged
# Python lists are references — assignment ALIASES: original = [1, 2, 3] alias = original alias.append(4) print(len(original)) # 4 — changed! real_copy = original.copy() # or original[:] real_copy.append(5) print(len(original)) # still 4
A crucial reversal: PHP arrays are value types, so $copy = $original duplicates the data, whereas Python lists and dicts are references, so alias = original makes two names for one object. To actually copy, call .copy(), slice with [:], or use copy.deepcopy() for nested structures.
Dict Keys Keep Their Type
<?php // PHP silently casts "1" and 1 to the SAME array key: $data = []; $data["1"] = "string key"; $data[1] = "int key"; echo count($data), "\n"; // 1 — they collided!
data = {} data["1"] = "string key" data[1] = "int key" print(len(data)) # 2 — distinct keys print(data) # {'1': 'string key', 1: 'int key'}
PHP normalises array keys, folding the string "1" and the integer 1 into one entry. Python keeps key types distinct, so "1" and 1 are two different dict keys. Any dict key must also be hashable (immutable) — a list cannot be a key, but a tuple can.