# Python Tooling for PHP Developers


Composer, PHPUnit, PHP-CS-Fixer, PHPStan. PHP developers already work with a specialized toolchain. Python's tooling is organized around the same concerns but splits them differently, and the differences in how each language handles isolation, versioning, and execution shape every tool in the stack. This guide maps PHP concepts to their Python equivalents and explains where the mental models diverge.

## What Will Feel Familiar, and What Will Not

Most PHP workflows have a recognizable Python counterpart:

| PHP | Python | Notes |
|-----|--------|-------|
| php-build / phpenv | `uv python install` | Python version management |
| Composer | [uv](https://pydevtools.com/handbook/reference/uv.md) | Dependency management, lock files, script running |
| `composer.json` | [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) | Central project configuration |
| `composer.lock` | `uv.lock` | Pinned dependency versions |
| Packagist | [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md) | Public package registry |
| `vendor/` | `.venv` (virtual environment) | Per-project dependency isolation |
| PHPUnit | [pytest](https://pydevtools.com/handbook/reference/pytest.md) | Testing framework |
| PHP-CS-Fixer / PHP_CodeSniffer | [Ruff](https://pydevtools.com/handbook/reference/ruff.md) | Linting and formatting |
| PHPStan / Psalm | [mypy](https://pydevtools.com/handbook/reference/mypy.md) / [pyright](https://pydevtools.com/handbook/reference/pyright.md) / [ty](https://pydevtools.com/handbook/reference/ty.md) | Static analysis and type checking |
| `composer global require` | `uv tool install` / `uvx` | System-wide CLI tools |

> [!IMPORTANT]
> These analogies are orientation aids, not exact equivalences. Each pairing hides real differences in scope and behavior.

Two mental model gaps will affect daily work.

Virtual environments are not `vendor/`. Composer's `vendor/` directory holds downloaded packages for one project. A Python [virtual environment](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md) goes further: it contains a Python interpreter (or symlink to one) and its own package directory, forming a self-contained execution context. Deleting and recreating a virtual environment is routine. You can also have multiple environments for the same project, each pinned to a different Python version.

Standards and tools are separate. PHP's packaging ecosystem is defined by Composer. Python separates standards (defined by [PEPs](https://pydevtools.com/handbook/explanation/pep.md)) from implementations, so multiple tools can implement the same interface. This is why [so many Python packaging tools exist](https://pydevtools.com/handbook/explanation/why-are-there-so-many-python-packaging-tools.md). uv covers most of the surface area, but the standards-vs-tools distinction explains choices that otherwise seem arbitrary.

## Python's Three-Layer Model

PHP collapses execution and dependency management into two pieces: a PHP binary and Composer. The PHP binary runs code directly, and Composer handles everything else (dependencies, autoloading, scripts). Python separates these concerns into three layers.

Layer 1: The interpreter. A specific version of CPython, managed with `uv python install 3.12`. Unlike PHP, where a single `php` binary handles all execution, Python version management is an explicit, separate operation. Multiple interpreters can coexist on one machine.

Layer 2: The virtual environment. A [virtual environment](https://pydevtools.com/handbook/explanation/what-is-a-virtual-environment.md) is a directory containing a Python executable (via symlink or copy) and its own `site-packages` folder for installed packages. It is attached to an existing interpreter but does not install one.

PHP relies on Composer's `vendor/autoload.php` for per-project isolation: include the autoloader and the project's dependencies become available. Python's isolation is at the process level. When a virtual environment is active, the `python` command itself resolves to that environment's interpreter, and only packages installed in that environment are importable. No autoloader needed.

Layer 3: The project definition. [pyproject.toml](https://pydevtools.com/handbook/reference/pyproject.toml.md) declares metadata, dependencies, and tool configuration. It plays a role similar to `composer.json`, though it is lighter: no autoload configuration and no post-install scripts by default. Tool configuration for linters, formatters, and test runners lives in `[tool.*]` sections within the same file.

This three-layer separation explains a design difference PHP developers notice quickly: `uv` bridges all three layers. It installs Python versions, creates virtual environments, resolves dependencies, and runs scripts. For PHP developers, think of it as if phpenv and Composer merged into one tool.

## The Daily Development Loop

### Adding and Managing Dependencies

Composer and uv follow the same pattern: declare, lock, install.

```bash
# Python (uv)                 # PHP (Composer)
uv add requests                # composer require guzzlehttp/guzzle
uv add --dev pytest            # composer require --dev phpunit/phpunit
uv sync                        # composer install
```

`uv add` updates `pyproject.toml` and writes a [lock file](https://pydevtools.com/handbook/explanation/what-is-a-lock-file.md) (`uv.lock`). `uv sync` installs from the lock file, the same workflow as `composer install` reading `composer.lock`.

Python has no direct equivalent of Composer's autoloading configuration (`autoload` and `autoload-dev` in `composer.json`). Python's import system resolves modules by searching directories on `sys.path`, and virtual environments manage which directories are on that path. There is nothing to configure.

Python also supports optional extras (published package metadata that consumers can request at install time, like `uv add "httpx[http2]"`) and [dependency groups](https://pydevtools.com/handbook/explanation/what-are-optional-dependencies-and-dependency-groups.md) for organizing dev, test, and documentation dependencies separately.

### Running Python Code

PHP runs files directly: `php index.php`. Python is similar:

```bash
uv run python main.py
uv run main.py               # also works for .py files
```

`uv run` ensures the virtual environment is active and dependencies are installed before executing the command. It fills the role of both `php` execution and the implicit `vendor/autoload.php` include.

Python has two execution modes that affect import resolution. `python file.py` runs a single file. `python -m package` runs a package as a module, which adjusts how imports resolve. When a project uses relative imports, the distinction matters. In PHP, the autoloader handles this transparently; in Python, how you invoke the code affects what it can import.

Console scripts (entry points defined in `pyproject.toml`) serve a similar role to Composer's `bin` declarations. They create named commands installed into the environment's `bin/` directory:

```toml
[project.scripts]
myapp = "mypackage.cli:main"
```

For library development, the `src/` layout keeps source code separate from configuration and tests. Combined with an [editable install](https://pydevtools.com/handbook/explanation/what-is-an-editable-install.md) (which `uv sync` handles automatically), code changes take effect without reinstalling.

### Code Quality: Linting and Formatting

PHP projects typically use PHP-CS-Fixer or PHP_CodeSniffer for formatting and code style, sometimes with separate tools for different concerns. Python consolidates linting and formatting into [Ruff](https://pydevtools.com/handbook/explanation/ruff-complete-guide.md):

```bash
uv run ruff check .          # lint (like php-cs-fixer fix --dry-run)
uv run ruff format .         # format (like php-cs-fixer fix)
```

Configuration lives in `pyproject.toml` under `[tool.ruff]`, not in a separate file like `.php-cs-fixer.php`:

```toml
[tool.ruff]
line-length = 88

[tool.ruff.lint]
select = ["E", "F", "I"]    # enable specific rule sets
```

Ruff re-implements rules from dozens of older Python linting tools, so most projects need only this single dependency for code quality.

### Type Checking

PHP and Python both adopted gradual typing after years without it. PHP added type declarations in 7.0 (scalar type hints) and has steadily expanded them through 8.x. Python's type annotations arrived in 3.5 and have grown similarly. The key difference: PHP's runtime enforces type declarations by default (a `TypeError` is thrown if an argument does not match). Python's runtime ignores annotations entirely. Type checking requires a separate tool.

Three type checkers are in active use: [mypy, pyright, and ty](https://pydevtools.com/handbook/explanation/how-do-mypy-pyright-and-ty-compare.md). PHP developers familiar with PHPStan or Psalm will recognize the workflow: run a static analyzer over the codebase, get a report of type errors, fix them. The difference is that Python's type checker is the only enforcement mechanism. There is no runtime fallback.

```bash
uv run mypy .                # like vendor/bin/phpstan analyse
```

Because typing is gradual, it is common to adopt it incrementally: add annotations to new code, tighten strictness over time, and leave legacy code untyped until it needs changes.

### Testing with pytest

PHPUnit organizes tests in classes that extend `TestCase`, with methods prefixed by `test`. [pytest](https://pydevtools.com/handbook/reference/pytest.md) takes a lighter approach: write plain functions whose names start with `test_`, and pytest discovers and runs them.

```python
# test_math.py
def test_addition():
    assert 1 + 1 == 2
```

No class hierarchy. No `$this->assertEquals()`. Python's built-in `assert` statement works directly, and pytest rewrites it at import time to produce detailed failure messages.

Where PHPUnit uses `setUp()` and `tearDown()` methods, pytest uses *fixtures*: functions decorated with `@pytest.fixture` that provide test dependencies through injection. Tests declare which fixtures they need as function parameters, and pytest wires them up automatically.

```bash
uv run pytest                # like vendor/bin/phpunit
```

## Packaging and Distribution

### Library Packaging

Composer packages are source distributions: the registry (Packagist) serves the source, and Composer downloads it. Python packages come in two forms: source distributions (sdists) and wheels (pre-built packages). When a wheel is available, installation skips the build step. A [build backend](https://pydevtools.com/handbook/explanation/what-is-a-build-backend.md) handles creating both formats.

Publishing to [PyPI](https://pydevtools.com/handbook/explanation/what-is-pypi.md) is conceptually similar to publishing on Packagist:

```bash
uv build                     # creates wheel and sdist
uv publish                   # uploads to PyPI
```

### Application Deployment

PHP applications run behind a web server (Nginx + PHP-FPM, Apache + mod_php) and deploy as source code. Python web applications are similar in that they deploy as source code, but the execution model differs: Python web frameworks (Django, Flask, FastAPI) typically run behind an application server (gunicorn, uvicorn) that the project declares as a dependency, rather than relying on a separate system-level runtime like PHP-FPM.

Deployment usually means a container image that installs dependencies with `uv sync --frozen` and runs the application server. There is no compilation or bundling step for pure Python applications.

### CLI Tools

Composer's `global require` installs a package system-wide. Python's equivalent is `uv tool install`, which creates an isolated environment per tool to avoid dependency conflicts between tools:

```bash
uv tool install ruff         # like composer global require friendsofphp/php-cs-fixer
uvx ruff check .             # run without permanent install (like composer exec)
```

## What Python Offers, and What PHP Developers Will Miss

Python's REPL and [IPython](https://pydevtools.com/handbook/reference/ipython.md) let developers test ideas interactively without writing a file, something PHP's `php -a` interactive mode supports in principle but rarely matches in practice. Jupyter notebooks extend this into a full document format used for data science and exploratory work. Tools like [tox](https://pydevtools.com/handbook/reference/tox.md) and [nox](https://pydevtools.com/handbook/reference/nox.md) automate testing across multiple Python versions in a single command.

PHP developers will miss a few things. Composer's `scripts` section provides a built-in task runner ("post-install-cmd", "test", custom scripts). Python has no equivalent in `pyproject.toml`; teams use Makefiles or `uv run` with specific commands. The autoloading system that makes classes available without explicit imports has no Python parallel; Python requires explicit `import` statements for every module. And while Python's ecosystem covers web, data science, ML, automation, and scripting, PHP's integration with web servers and the CMS/framework ecosystem (Laravel, WordPress) has no direct counterpart in Python's web story.
