Your short, to the point guide for writing modern Python code.
uv is ultra fast, makes managing virtual environments easy, and comes with many great features, like workspaces, or bytecode compilation to boost startup times.
To start using uv in your project, simply run:
uv init # create minimal pyproject.tomlDependency Groups & Development Dependencies:
To maintain a clean and lightweight Docker image for deployment, install development related tools and dependencies (like linters, formatters, testing frameworks) using the --dev flag:
uv add --dev <dev-dependency-name>Managing outdated dependencies:
You can locally list and update outdated dependencies using uv tree:
uv tree --depth 1 --outdatedYou should setup Rennovate to automatically upgrade dependencies.
ruff is incredibly fast and combines the functionalities of Black (formatter), isort (import sorter), and Flake8 (linter) into a single tool.
Install Ruff as a development dependency using uv:
uv add --dev ruff # install ruff in the dev dependencies groupConfiguring Ruff:
Ruff comes with great default settings, however to tweak it to your specific needs, simply add necessary sections in your pyproject.toml (refer to the Ruff settings documentation page for more info)
[tool.ruff]
exclude = ["alembic"]
line-length = 120
[tool.ruff.lint]
fixable = ["ALL"]
[tool.ruff.format]
quote-style = "double"Testing is essential and should be part of every Python project. Pytest is the recommended testing framework. You should also install the code coverage extension along with Pytest:
uv add --dev pytest pytest-covOn naming test files:
Pytest offers flexibility in test file organization. A clean approach is to place test files alongside your source code, named like test_*.py or *_test.py.
Automatic coverage:
For immediate feedback on test coverage, configure pytest-cov directly in your pyproject.toml:
[tool.pytest.ini_options]
filterwarnings = ["ignore::DeprecationWarning"]
addopts = "--cov=. --cov-report html"
[tool.coverage.run]
omit = ["*_test.py"]This setup will automatically generate an HTML coverage report in your project's htmlcov directory whenever you run pytest.
Here are the minimal recommended extensions to have a great development experience:
To make the most of these tools, configure your .vscode/settings.json file:
"[python]": {
"editor.formatOnSave": true,
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
}
}It's highly recommended to commit the .vscode/settings.json file to your project repository to ensure that everyone on the team uses the same consistent development settings.
Working in monorepos:
If your Python codebase is within a subfolder of a larger monorepo, VSCode offers a couple of approaches for proper configuration. You can either use VSCode's multi-root workspace feature to treat each subfolder as an independent project, or you can customize your workspace-level .vscode/settings.json to setup your Python code's location.
For exemple, assuming your python code is in a server subfolder:
{
"python.defaultInterpreterPath": "./server/.venv/bin/python",
"ruff.configuration": "./server/pyproject.toml",
"mypy-type-checker.cwd": "${workspaceFolder}/server",
"mypy-type-checker.args": ["--config-file", "pyproject.toml"],
"python.analysis.extraPaths": ["${workspaceFolder}/server"],
"python.autoComplete.extraPaths": ["${workspaceFolder}/server"]
}Typing is also essential and should be part of every Python project. It makes refactoring easier, catches errors early, and improves code readability for everyone (including LLMs!).
Add mypy to your project:
uv add --dev mypyConfiguring mypy:
To tweak mypy settings, add a section to your pyproject.toml:
[tool.mypy]
strict = truestrict_optional = truewarn_return_any = truewarn_unreachable = trueexclude = [".venv"]Refer to mypy documentation page for more info.
Python's type system includes built-in support for commonly used types. Here’s a quick overview:
str: for strings.int: for integers.float: for floating-point numbers.bool: for booleans.list[item_type]: for lists containing items of item_type.dict[key_type, value_type]: for dictionaries.tuple[type1, type2]: for tuples of fixed size.tuple[type1, ...]: for tuples of variable size.set[item_type]: for sets.type1 | type2:union type, for when a value can be one of a few types.type | None: optional type, for a value that can be none.
name: str="Alice"# stringage: int=30# integercrown_jewel_weight_kg: float=4.2# floatis_rebel: bool=True# booleanroyal_titles: list[str] = ["Princess of the forest", "Duchess of the sea"] # list of stringsknown_languages: dict[str, str] = {"elvish": "fluent", "common": "native"} # dictgarden_dimensions: tuple[str, int, int] = ("METER", 36, 6) # fixed-size typlefavorite_numbers: tuple[int, ...] = (7, 13, 21, 34) # variable-size typlecastle_rooms: set[str] = {"throne_room", "ballroom", "library"} # setknight_in_service: str|None="Sir Reginald"# either str or NoneType inference:
While MyPy can often infer types from assigned values, declaring types clearly, as shown in the example, greatly enhances code readability. It's a recommended practice for maintainable Python.
Runtime behavior:
Remember, type hints are primarily for static analysis by tools like MyPy and are not enforced at runtime.
You need an additional library to enforce types at runtime, like typeguard.
You should avoid using object or Any. If for some reasons you need an escape hatch or types are truly unknown (or dynamic) consider:
object: Base class for all classes. Only universally supported operations are allowed, offering some type safety.Any: Effectively disables type checking. Any operations are permitted on the value.
fromtypingimportAny# using objectvague_attribute: object="Crown Jewel"vague_attribute=3.14# Reassigning to a float - also valid with objectprint(vague_attribute.upper()) # Static type error! 'object' has no attribute 'upper'vague_attribute+5# Static type error! Binary operation "+" not defined for 'object' and 'int'print(vague_attribute.__str__()) # Okay - __str__ is a universal method for objectsprint(type(vague_attribute)) # Okay - type() works for all objects# using Anyflexible_attribute: Any="Royal Decree"flexible_attribute=42# Reassigning to an integer - perfectly valid with Anyprint(flexible_attribute.upper()) # No type error initially, runtime error if type is not strflexible_attribute+10# No type error, even if 'flexible_attribute' is a string laterIn strict mode, mypy will enforce you to type both function arguments and return values.
Here is how you would type a function:
defcalculate_sum(numbers: list[int]) ->int:
returnsum(numbers)Explicit None Return Type:
For functions that don't explicitly return a value, it's best practice to explicitly annotate the return type as None.
defprocess_data(data: list[str]) ->None:
# ... process the data ...Prefer immutable containers for arguments:
When a function is designed not to modify its input arguments, using immutable container types in the function signature is highly recommended.
list[item_type]: usetyping.Sequence[item_type], if you need random access via my_list[id] ortyping.Iterable[item_type]if you are just iterating over the list.dict[key_type, value_type]: usetyping.Mapping[key_type, value_type], if you need random access via my_dict[key] ortyping.Iterable[tuple[key_type, value_type]]if you are just iterating over the dictionaryset[item_type]: usetyping.AbstractSet[item_type]
Type Narrowing with typing.TypeIs:
A function returning a bool value can be used to narrow the type of a variable.
TypeIs can narrow the type in both the if and else branches of a conditional.
From the Python documentation:
fromtypingimportTypeIs, assert_typedefis_str(x: object) ->TypeIs[str]:
returnisinstance(x, str)
deff(x: str|int) ->None:
ifis_str(x):
assert_type(x, str)
else:
assert_type(x, int)Python's typing system isn't limited to built-in types. You can define your own custom types for increased clarity and type safety.
Litterals:
Use Literal to restrict a variable to a specific set of literal values (strings, numbers, booleans). Great for configuration options or constrained inputs.
fromtypingimportLiteraldefset_log_level(level: Literal["DEBUG", "INFO", "WARNING", "ERROR"]) ->None:
iflevel=="DEBUG":Enums:
For a more structured way to represent a fixed set of named values, use Enum from the enum module. Enums are more readable and maintainable than using plain strings or magic numbers.
classUserRole(str, Enum): # Inherit from str for easy JSON serializationADMIN="admin"EDITOR="editor"VIEWER="viewer"defgrant_permission(role: UserRole) ->None:
ifrole==UserRole.ADMIN:Classes:
Classes themselves act as custom types. When you define a class, you're creating a new type. Use class names directly as type hints.
Type alias:
Create type aliases to give descriptive names to complex types, improving readability, especially for nested or repetitive type hints.
type UserProfileData=Dict[str, List[Tuple[str, int]]]
user_data: UserProfileData= {
"alice": [("login_count", 5), ("last_activity", 1678886400)],
"bob": [("login_count", 12), ("last_activity", 1678972800)],
}Typings are continuously evolving, with features introduced in each new Python release.
The PEP (Python Enhancement Proposal) index for typing is a great place to keep an eye on upcoming features and understand the rationale behind existing ones.