SRE/Data Persistence/Documentation/Python
Appearance
Python development tips
This page documents Python development tips for the Data Persistence team and SREs in general.
If you starting a new project you might also consider https://gitlab.wikimedia.org/repos/data_persistence/service-template
General tips
- Use type hints and run mypy based on the configuration below
- Use ruff with the configuration below
- Use Pydantic on new projects for stronger type checking
- Use pytest with mock for testing
- Use caplog and syrupy to capture and check logs
Best practices
- Prefer using logging with levels and timestamps
- In complex scripts it can be difficult to understand who-logged-what: show the tool or function or target name frequently or in each log
- Use progress bars or terminal manipulation sparingly
- Either send that to stderr or disable it when stdout is redirected so that stdout can be captured in a "clean" state
pyproject.toml
Create pyproject.toml as:
# Tools are sorted by name
[tool.black]
line-length = 120
[tool.flake8]
max-line-length = 120
[tool.mypy]
# Fine-grained mypy configuration. You can incrementally enable stricter checks as you improve type hints.
check_untyped_defs = true
# disallow_any_generics = true
disallow_incomplete_defs = true
disallow_untyped_calls = true
follow_imports = "skip"
ignore_missing_imports = true
no_implicit_optional = true
pretty = true
show_error_context = true
strict_equality = true
#strict = true
warn_redundant_casts = true
# warn_return_any = true
warn_unused_configs = true
warn_unused_ignores = true
# disallow_any_expr = true
disallow_any_explicit = true
[tool.pylsp-mypy]
# Enable mypy integration in the Python LSP server
enabled = true
live_mode = true
# strict = true
[tool.pytest.ini_options]
norecursedirs = ["build", "dist", ".git", ".tox"]
[tool.ruff]
# Minimum python version to support
target-version = "py311"
# Exclude commonly ignored directories
exclude = [
".direnv",
".eggs",
".git",
".git-rewrite",
".mypy_cache",
".nox",
".pants.d",
".pyenv",
".pytest_cache",
".pytype",
".ruff_cache",
".tox",
".venv",
".vscode",
"__pypackages__",
"_build",
"build",
"dist",
"site-packages",
"venv",
]
line-length = 120
[tool.ruff.lint]
# You can incrementally enable stricter checks as needed
select = [
"E", # Pycodestyle errors
"F", # Pyflakes
"ERA", # Eradicate
"YTT", # Flake8 2020
"A", # Flake8 builtins
"T10", # Flake8 debugger
"EXE", # Flake8 executable
"G", # Flake8 logging format
"PIE", # Flake8 pie
"T20", # Flake8 print
"SLOT", # Flake8 slots
"TID", # Flake8 tidy imports
"I", # Isort
"W", # Pycodestyle warnings
# "S", # Flake8 bandit
# "B", # Flake8 bugbear
# "PL", # Pylint
#
# "ANN", # Flake8 annotations
# "BLE", # Flake8 blind except
# "FBT", # Flake8 boolean trap
# "COM", # Flake8 commas
# "C4", # Flake8 comprehensions
# "DTZ", # Flake8 datetimez
# "ISC", # Flake8 implicit string concat
# "INP", # Flake8 no pep420
# "PT", # Flake8 pytest style
# "SLF", # Flake8 self - private member access
# "SIM", # Flake8 simplify
# "ARG", # Flake8 unused arguments
# "PTH", # Flake8 use pathlib
# "RUF", # Ruff specific rules
# "TRY", # Tryceratops
]
ignore = [
"ANN001", # Missing type annotation for function argument
"ANN201", # Missing return type annotation for public function
"ANN202", # Missing return type annotation for private function
"ANN204", # Missing return type annotation for special method `__init__`
"ANN401", # Allow Any type hint
"C408", # Unnecessary `dict()` call (rewrite as a literal)
"COM812", # Trailing commas enforced by the formatter
"D200", # One-line docstring should fit on one line
"D205", # 1 blank line required between summary line and description
"D212", # Multi-line docstring summary should start at the first line
"D400", # First line should end with a period
"D415", # First line should end with a period, question mark, or exclamation point
"ERA001", # Commented-out code
"G004", # Logging statement uses f-string
"I001", # Import block is un-sorted
"PLR", # Ignore Pylint refactor rules - we run pylint directly
"PTH123", # Do not force to use Path in all cases
"Q000", # Allow to use the built-in open() function
"S101", # Use of `assert` detected
"S603", # Subprocess calls without shell=True
"SIM108", # Don't force ternary operator always
"T201", # Allow print() calls
"TRY003", # Allow exception classes that don't define a common message
"TRY400", # Allow to log just the error message and not the full exception traceback
]
[tool.ruff.lint.per-file-ignores]
"tests/*" = [
"ANN", # Ignore type hints checks in tests
"ARG001", # Unused function argument
"B011", # Do not `assert False`
"E501", # Long lines
"INP001", # File `tests/unit/test_import.py` is part of an implicit namespace package.
"PT015", # Assertion always fails, replace with `pytest.fail()`
"S101", # Allow asserts
"S106", # Allow hardcoded fake passwords in tests
]
[tool.ruff.lint.flake8-bandit]
check-typed-exception = true
[tool.ruff.format]
docstring-code-format = true
This replaces the need for dedicated conf files for mypy and tox.
Security
Where possible consider using only dependencies from Debian instead of pip/poetry install.