February 07, 2026

Common Python pitfalls for PHP developers (and how to avoid them)

Python isn’t hard for PHP developers. Thinking like a PHP developer in Python is. Here are the most common pitfalls and why they happen.

Common Python pitfalls for PHP developers (and how to avoid them)

When you come from PHP, learning Python can feel deceptively easy at first. Variables, functions, classes: nothing that shocking. Reading Python code is very easy.

And yet, after a few days or weeks coding a real program, things start to behave in unexpected ways.

Most of the time, Python isn’t being weird. You’re just still thinking like a PHP developer.

In this article, I’ll go through the most common pitfalls PHP developers run into when learning Python. Once you understand these, things feel less mysterious and magical.

1. The execution model is fundamentally different

This is the biggest conceptual shift for PHP developers.

PHP is quite special when it comes to its style of execution. An HTTP request comes, a php-fpm process is spawned, the request is handled, and the process then dies.

  • Every HTTP request starts from scratch
  • The whole runtime is created
  • Your code runs
  • Everything is destroyed at the end of the request

Unless you explicitly persisted some state to an external system (database, cache, session), the state is gone.

This means you can focus on the context of the request and not care about memory leaks.

In Python (as in Node.js and most other programming languages), the program keeps running and is reused across requests.

  • The program starts and keeps running
  • The running program is reused for each request
  • Modules are only imported once
  • State can leak across requests

If you only translate PHP code into Python but keeps the same architecture and coding patterns, memory-leaks are guaranteed and security issues may arise.

Here is an example of what a PHP developer could write:

Copied
Highlighting...
class ProductRepository
{
    private static array $cache = [];

    public function getProducts(
        string $category
    ): array {
        if (!isset(self::$cache[$category])) {
            self::$cache[$category] = $this->getProductsForCategory(
                $category
            );
        }
        
        return self::$cache[$category];
    }

    // ...
}

This works perfectly in PHP because $cache is destroyed after each request. A PHP developer familiar with static variables might write similar Python code:

Copied
Highlighting...
# Module level - executed once when imported
_product_cache = {}

def get_products(category: str):
    # BUG: This cache grows forever across all requests!
    if category not in _product_cache:
        _product_cache[category] = get_products_for_category(category)
    
    return _product_cache[category]

The problem: _product_cache is created once when the module loads and persists across all requests forever. It grows indefinitely with every unique category ever requested. In PHP, the static variable is scoped to the request lifecycle, so this same pattern is safe.

2. Mutability will bite you sooner or later

Python declares default parameters only once (when the function is defined). With mutable types, it can lead to terrible bugs.

A classic example:

Copied
Highlighting...
def add_item(item, items=[]):
    items.append(item)

    return items

For a PHP developer, this looks harmless. In Python, that default list is created once, not per call.

So state accumulates across calls.

Copied
Highlighting...
# ['a']
add_item('a')
# ['a', 'b']
add_item('b')
# ['a', 'b', 'c']
add_item('c')

You have to be very careful about default parameters: they don't look dangerous at first but can lead to many "wtf" moments.

Python developers usually deal with it like this:

Copied
Highlighting...
def add_item(item, items=None):
    if items is None:
        items = []

    items.append(item)

    return items

# ['a']
add_item('a')
# ['b']
add_item('b')
# ['c']
add_item('c')

3. Not using virtual environments and modern package managers

In PHP, composer handles both dependency management and autoloading in a straightforward way. Python's ecosystem is more fragmented, which catches PHP developers off guard. PHP developers often start by installing packages globally with pip install, the same way they might have started with Composer. But unlike PHP where each project naturally isolates its dependencies through vendor/, Python requires explicit virtual environments.

Without virtual environments, you'll run into version conflicts between projects and polluted global python installation.

The modern Python approach uses virtual environments, created manually through the built-in venv module or indirectly by package managers like uv or poetry.

Think of virtual environments like having a separate vendor/ directory that also includes the Python interpreter itself.

4. Looking for interfaces

PHP developers love interfaces. They're how we define contracts, enable dependency injection, and make code testable.

Python doesn't have interfaces in the traditional sense. At first, this feels wrong. How do you ensure a class implements certain methods? How do you do proper dependency injection?

Python relies on duck typing: "If it walks like a duck and quacks like a duck, it's a duck."

You don't declare that a class implements an interface; you just use it. If it has the methods you call, it works.

Modern Python does have tools that bridge this gap:

  • Abstract Base Classes (ABC) for enforcing method implementation
  • Protocol classes (Python 3.8+) for structural subtyping

But the Python way is to embrace duck typing for flexibility and use these stricter approaches only when you need them: not by default like in PHP.

5. Type hints

PHP type hints are enforced at runtime.

Copied
Highlighting...
function greet(string $name): string {
    return "Hello, " . $name;
}

greet(123); // Fatal error: Argument must be of type string

Python's runtime doesn't care about them (although some tools like Pydantic can rely on them at runtime).

Copied
Highlighting...
def greet(name: str) -> str:
    return f"Hello, {name}"

# "Hello, 123"
greet(123)  # Runs fine! No error at runtime

Python type hints exist for static analysis tools, not the runtime. Tools like mypy, pyright, or your IDE will catch type errors before you run the code — similar to how TypeScript works.

Why this matters:

  • You need to actually run a type checker (it won't happen automatically)
  • Type hints are optional — you can mix typed and untyped code
  • Some libraries use type hints at runtime (like Pydantic, FastAPI), but that's the exception

Think of Python type hints like TypeScript: helpful for development and catching bugs early, but not a runtime guarantee. The flexibility is powerful once you adjust your mental model.

6. Not writing pythonic code

PHP developers often translate their patterns directly: lots of classes, explicit getters/setters, verbose loops. Python has idioms that feel strange at first but become natural:

List comprehensions instead of foreach loops:

Copied
Highlighting...
# PHP-style Python (verbose)
items = []
for user in users:
    items.append(user.name)

# Pythonic
items = [user.name for user in users]

Context managers instead of try/finally:

Copied
Highlighting...
# PHP-style
file = open('data.txt')
try:
    content = file.read()
finally:
    file.close()

# Pythonic
with open('data.txt') as file:
    content = file.read()

Decorators for reusable concerns:

Copied
Highlighting...
@login_required
@cache(ttl=300)
def get_user_profile(user_id):
    return fetch_profile(user_id)

7. Being reluctant to monkey patching

In PHP, modifying third-party code means forking packages or extending classes. Python allows you to modify classes and modules at runtime. This is called monkey patching. PHP developers initially see this as dangerous and hacky. And yes, it can be when abused. But it's also a powerful tool for testing (mocking dependencies without dependency injection containers).

Copied
Highlighting...
# Mock a dependency in tests
from unittest.mock import patch

def test_my_function():
    api_response = {
        "user": {
            "id": 1,
            "username": "John"
        }
    }
    with patch('external_library.api_call', return_value=api_response):
        result = my_function()

        assert result == {"id": 1, "username": "John"}
    # Automatically restored after the context

The Python community has conventions around when monkey patching is acceptable (mostly in tests and development tools). Learning to use it appropriately, rather than avoiding it entirely, makes you more effective.

Conclusion

If you recognize yourself in these pitfalls, that’s a good sign.

They don’t mean you’re bad at Python. They mean you’re an experienced developer, just in a different ecosystem.

Having a guide in this process, knowing exactly where you could fall into traps, is a major accelerator in your learning.

In From PHP to Python, these differences are explained step by step, with side-by-side examples designed specifically for PHP developers.