Skip to content
Dimitri Merejkowsky edited this page Mar 17, 2026 · 8 revisions

Python Code Manifesto

Use a lock file

Counter examples

# In setup.py
setup(

    intall_requires=["django >= 5.0"]
)
# In dev-requirements.txt
black

Instead, use a tool that can generate a lock file, like poetry

That way:

When Django issues a new release that fixes a security issue, you can generate the lock and make sure your application is no longer vulnerable

Or, in 2027 when a new, incompatible version of black is released, you won't have to deal with people using different versions of black.

Use pyproject.toml for everything

It used to be you had to configure your Python project metadata in setup.py, setup.cfg or some dedicated configuration file (like mypy.ini)

Nowadays, it's recommended to store everything int he pyproject.toml instead.

Rationale:

  • Having only one configuration file is easier

  • Instead of having an ad-hoc configuration file loosly based on the INI syntax, you use a well documented format : TOML

For instance, you can put the isort configuration in pyproject.toml:

[tool.isort]
profile = "black"

Note: this works for almost all Python tools, flake8 being one of the few exceptions ...

Prefer using keyword-only arguments

Counter example:

# Definition:
def update_object(instance, attribute, in_batch=False): ...


# Call site:
update_object(pipe, "diameter")
update_object(pipe, "diameter", True)

In addition to be hard to read, breakage can happen if a new positional argument is added to the update_object function.

Suggested alternative

Use a bare * to signify "keyword-only" arguments:

# Definition
def update_object(instance, *, name, attribute, in_batch): ...


# Calls:
update_object(pipe, attribute="diameter")
update_object(pipe, attribute="diameter", in_batch=False)

Type annotations

We view type annotations as comments, and as the saying goes "incorrect comments are worse than no comments"

Here's a simple example:

def find_book_in_database(id: str) -> Book:
    row = db.execute("SELECT * FROM book WHERE id=?", (id))
    if not row;
        return None
    return Boo.from_row(row)

Here the type annotation seems to indicate that the function never returns None, but it's not true.

Worse, the IDE will happily provide auto completion when you call the function, making it hard to think about the fact this method can return None.

So, you have two choices:

  • Either, use mypy in strict mode and prevent pushing code when mypy fails.
def find_book_in_database(id: str) -> Book | None:
    ...
  • Or, don't use type annotations at all and document the None case in the doc string
def find_book_in_database(id):
    """ Retrieve a book from the database

    @returns None if the book is not found
    """

Using annotations without a strict type checker is not recommended, because it becomes too easy to have outdated type annotations when the code changes, and in our experience, it's a good way to generate bugs.

This also means we don't recommend using pydantic, dataclass, or FastAPI if you don't use mypy in strict mode, because those libraries force you to use type annotations.

Choosing a type checker

They are numerous type checkers available for Python, we recommend using mypy.

  • It seems to be the defacto standard
  • Microsoft, Meta, Google and Astral have each their own solution, but they have all different behavior from mypy - see this article for details.

Sticking to mypy seems like a good idea, because it's used and developed at Dropbox, and they have huge incentives to keep it working :)

Recommendations for using mypy

First, Read the fine documentation :)

Configuration

If you are in a hurry, here's a simple configuration, assuming you have a project named "book_store" and the tests in a separate folder;

[tool.mypy]
packages = [
    "book_store",
    "tests",
]
strict = true

If you get errors about third party libraries, read the section about missing imports and decide what to do.

We recommend using

[[tool.mypy.overrides]]
module = [
    "some_library"
]
ignore_missing_imports = true

only in last resort.

Using strict mode will force you to:

  • Annotate every parameter of every function and every return type
  • Annotate every variable which type cannot be deduced

We see this as a feature, not a bug.

Using plugins

If you use a library like pydantic or Django, you may find that mypy does not work as well as you would expect.

This is because it both cases you need to configure a plugin to improve the type checks:

Clone this wiki locally