Articles/Python/Python for PHP Developers

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.

June 15, 2026·12 min read

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.

PHP
// composer.json lists dependencies, then: composer install
require __DIR__ . '/vendor/autoload.php';

echo "Hello from PHP\n";
PYTHON
# 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:

BASH
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.

PHP
$name = "Ada";
$count = 3;
echo "Hello, {$name}! You have {$count} messages.\n";
// a comment
PYTHON
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.

PHP
function greet(string $name, ?int $times = 1): string
{
    return str_repeat("hi $name ", $times);
}
PYTHON
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.

PHP
$nums = [1, 2, 3];
$user = ["name" => "Ada", "role" => "admin"];

foreach ($nums as $n) { echo $n; }
foreach ($user as $key => $value) { echo "$key=$value"; }
PYTHON
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:

PHP
$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);
PYTHON
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.

PHP
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
PYTHON
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.

PHP
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}";
    }
}
PYTHON
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:

PYTHON
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):

PHP
$label = match ($status) {
    200, 201 => "ok",
    404 => "not found",
    default => "unknown",
};
PYTHON
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:

PHP
enum Status: string
{
    case Active = "active";
    case Closed = "closed";
}
echo Status::Active->value;
PYTHON
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:

PYTHON
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.

PHP
// src/Billing/Money.php declares: namespace App\Billing;
use App\Billing\Money;

$m = new Money(500);
PYTHON
# 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:

PYTHON
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:

PHP
try {
    risky();
} catch (RuntimeException $e) {
    log($e->getMessage());
} finally {
    cleanup();
}
PYTHON
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.