Python for PHP Developers
A friendly tour of Python for developers who already know modern PHP. We map the things you reach for every day, types, arrays, classes, named arguments, match, and enums, onto their Python equivalents so you can read and write Python with confidence.
If you write modern PHP, you already think in the right shapes: types, classes, enums, match, named arguments, immutability. Python has all of those too, it just spells them differently, with indentation instead of braces and snake_case instead of camelCase. This guide maps what you know onto Python so you can start reading and writing it quickly, without relearning how to program. If you followed my AWS Lambda series, those handlers were Python; this is the language behind them.
Running code and managing packages
In PHP you declare dependencies in composer.json, run composer install, get a vendor/ folder and an autoloader, and run a script with php script.php. Python is the same idea with different nouns.
// composer.json lists dependencies, then: composer install
require __DIR__ . '/vendor/autoload.php';
echo "Hello from PHP\n";
# requirements.txt lists dependencies. Work inside a virtual environment:
print("Hello from Python")
The one new habit is the virtual environment. Instead of a per-project vendor/, you create and "activate" an isolated environment so each project has its own packages:
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install requests
python hello.py
Think of venv as a vendor/ directory you switch on, pip as Composer, and requirements.txt as the dependency list. The other immediate shift: no $ on variables, no semicolons, and no curly braces. Indentation is the block syntax, so a misaligned line is a real error, not a style nit.
Syntax basics
Less ceremony, same ideas. The thing you will miss most at first is string interpolation, and Python's f-strings fill the gap nicely.
$name = "Ada";
$count = 3;
echo "Hello, {$name}! You have {$count} messages.\n";
// a comment
name = "Ada"
count = 3
print(f"Hello, {name}! You have {count} messages.")
# a comment
An f-string is any string prefixed with f, and {} interpolates an expression, much like "{$var}" in PHP. print() adds a newline for you, so you rarely type \n.
Types and type hints
Everything you learned about types in the PHP 8 era carries over, and the syntax is close enough to read on sight.
function greet(string $name, ?int $times = 1): string
{
return str_repeat("hi $name ", $times);
}
def greet(name: str, times: int | None = 1) -> str:
return f"hi {name} " * (times or 1)
?int becomes int | None, the parameter type goes after a colon, and the return type follows ->. The one crucial difference: Python does not enforce these hints at runtime. They document intent and power tools like mypy and your editor, but Python will happily run with the wrong type. PHP actually checks them. So in Python, hints are a strong convention rather than a guarantee.
Arrays become lists and dicts
This is the biggest mental adjustment. PHP's single array type does two jobs: ordered lists and keyed maps. Python splits them into two types, a list and a dict.
$nums = [1, 2, 3];
$user = ["name" => "Ada", "role" => "admin"];
foreach ($nums as $n) { echo $n; }
foreach ($user as $key => $value) { echo "$key=$value"; }
nums = [1, 2, 3] # a list
user = {"name": "Ada", "role": "admin"} # a dict
for n in nums:
print(n)
for key, value in user.items():
print(f"{key}={value}")
You iterate a dict's pairs with .items() rather than as $key => $value. And the array functions you use to avoid loops have direct counterparts, though the idiomatic Python is usually a comprehension:
$doubled = array_map(fn ($n) => $n * 2, $nums);
$evens = array_filter($nums, fn ($n) => $n % 2 === 0);
$total = array_reduce($nums, fn ($carry, $n) => $carry + $n, 0);
doubled = [n * 2 for n in nums] # list comprehension
evens = [n for n in nums if n % 2 == 0]
total = sum(nums) # functools.reduce exists for the general case
A list comprehension reads as "this expression, for each item, optionally where a condition holds." Once it clicks, it replaces most of your array_map/array_filter reflexes. (For more on that PHP style, see array_map without loops.)
Functions
Defaults, named arguments, and variadics all have clean equivalents. PHP 8's named arguments in particular map almost one to one.
function box(int $w, int $h, string $color = "black"): array
{
return compact("w", "h", "color");
}
$b = box(w: 4, h: 3, color: "red"); // named arguments (PHP 8.0)
function total(...$nums): int { return array_sum($nums); }
$add = fn ($a, $b) => $a + $b; // arrow function
def box(w: int, h: int, color: str = "black") -> dict:
return {"w": w, "h": h, "color": color}
b = box(w=4, h=3, color="red") # keyword arguments
def total(*nums): # *args collects extra positionals
return sum(nums)
def add(a, b): # prefer def; lambda exists for one-liners
return a + b
PHP's name: value named arguments become name=value keyword arguments. Variadic ...$nums becomes *nums, and Python adds **kwargs for collecting keyword arguments into a dict. Arrow functions (fn () =>) have a lambda equivalent, but a named def is usually clearer and is what you will see most.
Classes and objects
Python is object-oriented to its core, so this will feel familiar, with two differences worth internalizing: self is explicit, and "private" is a convention rather than a rule.
final class Money
{
public function __construct(
public readonly int $amount,
private string $currency = "USD",
) {}
public function formatted(): string
{
return number_format($this->amount / 100, 2) . " {$this->currency}";
}
}
from dataclasses import dataclass
@dataclass(frozen=True)
class Money:
amount: int
currency: str = "USD"
def formatted(self) -> str:
return f"{self.amount / 100:.2f} {self.currency}"
A @dataclass is Python's answer to constructor property promotion: it generates the initializer from the typed fields. frozen=True makes it readonly. Notice every method takes self as its first parameter, where PHP gives you $this implicitly. If you prefer the long form, it looks like this:
class Money:
def __init__(self, amount: int, currency: str = "USD"):
self.amount = amount
self._currency = currency # a leading _ means "internal, please do not touch"
There is no real private or protected. A single leading underscore signals "internal," and a double underscore name-mangles to discourage access, but nothing stops a determined caller. Python's culture leans on convention where PHP leans on the compiler.
Modern features you already use
The recent additions to PHP that you reached for in the modern SOLID era have direct Python analogues.
match exists in both, and Python's version does even more (it can destructure):
$label = match ($status) {
200, 201 => "ok",
404 => "not found",
default => "unknown",
};
match status:
case 200 | 201:
label = "ok"
case 404:
label = "not found"
case _:
label = "unknown"
Enums map cleanly too. PHP 8.1's backed enums become an Enum subclass:
enum Status: string
{
case Active = "active";
case Closed = "closed";
}
echo Status::Active->value;
from enum import Enum
class Status(Enum):
ACTIVE = "active"
CLOSED = "closed"
print(Status.ACTIVE.value)
The one place PHP is more ergonomic is null handling. Python has no ?-> or ??, so you write the check out:
name = "guest"
if user is not None and user.profile is not None:
name = user.profile.name
For a simple default, value or fallback works, but be careful: Python's or falls back on any falsy value (0, "", []), not just None, so it is not a true null coalescing operator.
Modules and imports
PHP has namespaces, a use keyword, and an autoloader that maps class names to files. Python is simpler and more physical: a .py file is a module, a folder of them is a package, and you import by path.
// src/Billing/Money.php declares: namespace App\Billing;
use App\Billing\Money;
$m = new Money(500);
# billing/money.py is the module "billing.money"
from billing.money import Money
m = Money(500)
There is no autoloader to configure and no namespace declaration inside the file. The directory structure is the namespace, and from package.module import Thing is your use.
Idioms and gotchas
A few things will trip you up coming from PHP, so here they are up front.
Naming. PEP 8, Python's style guide, wants snake_case for functions and variables and PascalCase for classes. After years of PHP's camelCase methods this feels odd, but it is universal in Python.
The mutable default argument. This is the classic trap. A default value is created once, not per call, so a mutable default is shared across every call:
def add_item(item, basket=[]): # BUG: every call reuses the same list
basket.append(item)
return basket
def add_item(item, basket=None): # fix: default to None, build inside
basket = basket if basket is not None else []
basket.append(item)
return basket
is versus ==. Use == to compare values (as you would in PHP) and is only for identity, in practice for None checks: if x is None.
Errors. It is try/except/finally, not catch, and you raise rather than throw:
try {
risky();
} catch (RuntimeException $e) {
log($e->getMessage());
} finally {
cleanup();
}
try:
risky()
except RuntimeError as e:
log(str(e))
finally:
cleanup()
Batteries included. Python's standard library is broad. Reading JSON, handling dates, building paths, making HTTP requests, much of what you might pull a Composer package for is already there in modules like json, datetime, and pathlib.
You already think in types, objects, immutability, enums, and pattern matching. Python just asks you to spell them with indentation and underscores. The fastest way to make it stick is to build something small and real, and the standard library makes that genuinely quick.