Static typing and mypy
Python is dynamic, but in ZEUS we treat type annotations as a mandatory contract. Types catch bugs before they reach production, document a function's API and let the IDE give sensible suggestions. They are checked by mypy — in strict mode.
Basic annotations
def normalize_forest(name: str) -> str:
return name.strip().lower()
async def fetch_hosts(forest: str, limit: int = 100) -> list[str]:
...
From Python 3.9+ we use the built-in generics (list[str], dict[str, int])
rather than List/Dict from typing. From 3.10+ we write unions with |.
# old style (don't use)
from typing import List, Optional
def f(x: Optional[int]) -> List[str]: ...
# our style
def f(x: int | None) -> list[str]: ...
Optional, None and Pydantic
X | None is a type that may be None. mypy will force you to handle that
case — and that's the point.
def get_probe(probe_id: str) -> Probe | None:
return repo.find(probe_id)
probe = get_probe(pid)
print(probe.host) # mypy: error — probe may be None
if probe is not None:
print(probe.host) # OK — type narrowing
Typing async functions and ARQ
In ARQ tasks and FastAPI handlers we type both the arguments and the return value:
async def enqueue_scan(ctx: dict[str, Any], forest: str) -> int:
job = await ctx["redis"].enqueue_job("run_scan", forest)
return await job.result()
The ProfessNet standard — strict mypy
[tool.mypy]
python_version = "3.12"
strict = true
warn_unused_ignores = true
disallow_untyped_defs = true
plugins = ["pydantic.mypy"]
ProfessNet standard: new code is written under
strict = true. Thepydantic.mypyplugin understands Pydantic models and validates fields.
Any is technical debt
Any turns off type checking — use it deliberately and rarely. When you must
silence an error temporarily, add a comment with the reason:
data = parse(raw) # type: ignore[no-any-return] # external lib without stubs
| Construct | When |
|---|---|
| `X | None` |
Sequence[X] | read-only argument (list or tuple) |
Protocol | a behavioral contract without inheritance |
TypedDict | a dict with known keys (e.g. a JSON payload) |
Any | last resort, always with a comment |
Running it
mypy app/
We run mypy in CI alongside ruff and black. A red mypy = a blocked PR.
Tip: if a library has no types, install the
types-...package (e.g.types-requests) instead of burying the code intype: ignore.
Typing costs a minute while you write and saves hours of debugging in production. In ZEUS, typed code that passes mypy is a condition of entry to the main branch.