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. The pydantic.mypy plugin 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
ConstructWhen
`XNone`
Sequence[X]read-only argument (list or tuple)
Protocola behavioral contract without inheritance
TypedDicta dict with known keys (e.g. a JSON payload)
Anylast 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 in type: 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.