I built an ultra-strict typing setup in Python (FastAPI + LangGraph + Pydantic + Pyright + Ruff) 🚀

Posted by Ranteck@reddit | Python | View on Reddit | 6 comments

Hey everyone,

I recently worked on a project using FastAPI + LangGraph, and I kept running into typing headaches. So I went down the rabbit hole and decided to build the strictest setup I could, making sure no Any could sneak in.

Here’s the stack I ended up with:

What I gained:

Here’s my pyproject.toml if anyone wants to copy, tweak, or criticize it:

# ============================================================
# ULTRA-STRICT PYTHON PROJECT TEMPLATE
# Maximum strictness - TypeScript strict mode equivalent
# Tools: uv + ruff + pyright/pylance + pydantic v2
# Python 3.12+
# ============================================================

[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "your-project-name"
version = "0.1.0"
description = "Your project description"
authors = [{ name = "Your Name", email = "your.email@example.com" }]
license = { text = "MIT" }
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
    "pydantic",
    "pydantic-ai-slim[openai]",
    "types-requests",
    "python-dotenv",
]

[project.optional-dependencies]
dev = [
    "pyright",
    "ruff",
    "gitingest",
        "poethepoet"
]

[tool.setuptools.packages.find]
where = ["."]
include = ["*"]
exclude = ["tests*", "scripts*", "docs*", "examples*"]

# ============================================================
# POE THE POET - Task Runner
# ============================================================
[tool.poe.tasks]
# Run with: poe format or uv run poe format
# Formats code, fixes issues, and type checks
format = [
    {cmd = "ruff format ."},
    {cmd = "ruff check . --fix"},
    {cmd = "pyright"}
]

# Run with: poe check
# Lint and type check without fixing
check = [
    {cmd = "ruff check ."},
    {cmd = "pyright"}
]

# Run with: poe lint or uv run poe lint
# Only linting, no type checking
lint = {cmd = "ruff check . --fix"}

# Run with: poe lint-unsafe or uv run poe lint-unsafe
# Lint with unsafe fixes enabled (more aggressive)
lint-unsafe = {cmd = "ruff check . --fix --unsafe-fixes"}

# ============================================================
# RUFF CONFIGURATION - MAXIMUM STRICTNESS
# ============================================================
[tool.ruff]
target-version = "py312"
line-length = 88
indent-width = 4
fix = true
show-fixes = true

[tool.ruff.lint]
# Comprehensive rule set for strict checking
select = [
    "E",      # pycodestyle errors
    "F",      # pyflakes
    "I",      # isort
    "UP",     # pyupgrade
    "B",      # flake8-bugbear
    "C4",     # flake8-comprehensions
    "T20",    # flake8-print (no print statements)
    "SIM",    # flake8-simplify
    "N",      # pep8-naming
    "Q",      # flake8-quotes
    "RUF",    # Ruff-specific rules
    "ASYNC",  # flake8-async
    "S",      # flake8-bandit (security)
    "PTH",    # flake8-use-pathlib
    "ERA",    # eradicate (commented-out code)
    "PL",     # pylint
    "PERF",   # perflint (performance)
    "ANN",    # flake8-annotations
    "ARG",    # flake8-unused-arguments
    "RET",    # flake8-return
    "TCH",    # flake8-type-checking
]

ignore = [
    "E501",    # Line too long (formatter handles this)
    "S603",    # subprocess without shell=True (too strict)
    "S607",    # Starting a process with a partial path (too strict)
]

# Per-file ignores
[tool.ruff.lint.per-file-ignores]
"__init__.py" = [
    "F401",    # Allow unused imports in __init__.py
]
"tests/**/*.py" = [
    "S101",    # Allow assert in tests
    "PLR2004", # Allow magic values in tests
    "ANN",     # Don't require annotations in tests
]

[tool.ruff.lint.isort]
known-first-party = ["your_package_name"]  # CHANGE THIS
combine-as-imports = true
force-sort-within-sections = true

[tool.ruff.lint.pydocstyle]
convention = "google"

[tool.ruff.lint.flake8-type-checking]
strict = true

[tool.ruff.format]
quote-style = "double"
indent-style = "space"
skip-magic-trailing-comma = false
line-ending = "auto"

# ============================================================
# PYRIGHT CONFIGURATION - MAXIMUM STRICTNESS
# TypeScript strict mode equivalent
# ============================================================
[tool.pyright]
pythonVersion = "3.12"
typeCheckingMode = "strict"

# ============================================================
# IMPORT AND MODULE CHECKS
# ============================================================
reportMissingImports = true
reportMissingTypeStubs = true  # Stricter: require type stubs
reportUndefinedVariable = true
reportAssertAlwaysTrue = true
reportInvalidStringEscapeSequence = true

# ============================================================
# STRICT NULL SAFETY (like TS strictNullChecks)
# ============================================================
reportOptionalSubscript = true
reportOptionalMemberAccess = true
reportOptionalCall = true
reportOptionalIterable = true
reportOptionalContextManager = true
reportOptionalOperand = true

# ============================================================
# TYPE COMPLETENESS (like TS noImplicitAny + strictFunctionTypes)
# ============================================================
reportMissingParameterType = true
reportMissingTypeArgument = true
reportUnknownParameterType = true
reportUnknownLambdaType = true
reportUnknownArgumentType = true   # STRICT: Enable (can be noisy)
reportUnknownVariableType = true   # STRICT: Enable (can be noisy)
reportUnknownMemberType = true     # STRICT: Enable (can be noisy)
reportUntypedFunctionDecorator = true
reportUntypedClassDecorator = true
reportUntypedBaseClass = true
reportUntypedNamedTuple = true

# ============================================================
# CLASS AND INHERITANCE CHECKS
# ============================================================
reportIncompatibleMethodOverride = true
reportIncompatibleVariableOverride = true
reportInconsistentConstructor = true
reportUninitializedInstanceVariable = true
reportOverlappingOverload = true
reportMissingSuperCall = true  # STRICT: Enable

# ============================================================
# CODE QUALITY (like TS noUnusedLocals + noUnusedParameters)
# ============================================================
reportPrivateUsage = true
reportConstantRedefinition = true
reportInvalidStubStatement = true
reportIncompleteStub = true
reportUnsupportedDunderAll = true
reportUnusedClass = "error"        # STRICT: Error instead of warning
reportUnusedFunction = "error"     # STRICT: Error instead of warning
reportUnusedVariable = "error"     # STRICT: Error instead of warning
reportUnusedImport = "error"       # STRICT: Error instead of warning
reportDuplicateImport = "error"    # STRICT: Error instead of warning

# ============================================================
# UNNECESSARY CODE DETECTION
# ============================================================
reportUnnecessaryIsInstance = "error"         # STRICT: Error
reportUnnecessaryCast = "error"               # STRICT: Error
reportUnnecessaryComparison = "error"         # STRICT: Error
reportUnnecessaryContains = "error"           # STRICT: Error
reportUnnecessaryTypeIgnoreComment = "error"  # STRICT: Error

# ============================================================
# FUNCTION/METHOD SIGNATURE STRICTNESS
# ============================================================
reportGeneralTypeIssues = true
reportPropertyTypeMismatch = true
reportFunctionMemberAccess = true
reportCallInDefaultInitializer = true
reportImplicitStringConcatenation = true  # STRICT: Enable

# ============================================================
# ADDITIONAL STRICT CHECKS (Progressive Enhancement)
# ============================================================
reportImplicitOverride = true    # STRICT: Require @override decorator (Python 3.12+)
reportShadowedImports = true     # STRICT: Detect shadowed imports
reportDeprecated = "warning"     # Warn on deprecated usage

# ============================================================
# ADDITIONAL TYPE CHECKS
# ============================================================
reportImportCycles = "warning"

# ============================================================
# EXCLUSIONS
# ============================================================
exclude = [
    "**/__pycache__",
    "**/node_modules",
    ".git",
    ".mypy_cache",
    ".pyright_cache",
    ".ruff_cache",
    ".pytest_cache",
    ".venv",
    "venv",
    "env",
    "logs",
    "output",
    "data",
    "build",
    "dist",
    "*.egg-info",
]

venvPath = "."
venv = ".venv"

# ============================================================
# PYTEST CONFIGURATION
# ============================================================
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
    "--strict-markers",
    "--strict-config",
    "--tb=short",
    "--cov=.",
    "--cov-report=term-missing:skip-covered",
    "--cov-report=html",
    "--cov-report=xml",
    "--cov-fail-under=80",  # STRICT: Require 80% coverage
]
markers = [
    "slow: marks tests as slow (deselect with '-m \"not slow\"')",
    "integration: marks tests as integration tests",
    "unit: marks tests as unit tests",
]

# ============================================================
# COVERAGE CONFIGURATION
# ============================================================
[tool.coverage.run]
source = ["."]
branch = true  # STRICT: Enable branch coverage
omit = [
    "*/tests/*",
    "*/test_*.py",
    "*/__pycache__/*",
    "*/.venv/*",
    "*/venv/*",
    "*/scripts/*",
]

[tool.coverage.report]
precision = 2
show_missing = true
skip_covered = false
fail_under = 80  # STRICT: Require 80% coverage
exclude_lines = [
    "pragma: no cover",
    "def __repr__",
    "raise AssertionError",
    "raise NotImplementedError",
    "if __name__ == .__main__.:",
    "if TYPE_CHECKING:",
    "@abstractmethod",
    "@overload",
]

# ============================================================
# QUICK START GUIDE
# ============================================================
#
# 1. CREATE NEW PROJECT:
#    mkdir my-project && cd my-project
#    cp STRICT_PYPROJECT_TEMPLATE.toml pyproject.toml
#
# 2. CUSTOMIZE (REQUIRED):
#    - Change project.name to "my-project"
#    - Change project.description
#    - Change project.authors
#    - Change tool.ruff.lint.isort.known-first-party to ["my_project"]
#
# 3. SETUP ENVIRONMENT:
#    uv venv
#    source .venv/bin/activate  # Linux/Mac
#    .venv\Scripts\activate     # Windows
#    uv pip install -e ".[dev]"
#
# 4. CREATE PROJECT STRUCTURE:
#    mkdir -p src/my_project tests
#    touch src/my_project/__init__.py
#    touch tests/__init__.py
#
# 5. CREATE .gitignore:
#    echo ".venv/
#    __pycache__/
#    *.py[cod]
#    .pytest_cache/
#    .ruff_cache/
#    .pyright_cache/
#    .coverage
#    htmlcov/
#    dist/
#    build/
#    *.egg-info/
#    .env
#    .DS_Store" > .gitignore
#
# 6. DAILY WORKFLOW:
#    # Format code
#    uv run ruff format .
#
#    # Lint and auto-fix
#    uv run ruff check . --fix
#
#    # Type check (strict!)
#    uv run pyright
#
#    # Run tests with coverage
#    uv run pytest
#
#    # Full check (run before commit)
#    uv run ruff format . && uv run ruff check . && uv run pyright && uv run pytest
#
# 7. VS CODE SETUP (recommended):
#    Create .vscode/settings.json:
#    {
#      "python.defaultInterpreterPath": ".venv/bin/python",
#      "python.analysis.typeCheckingMode": "strict",
#      "python.analysis.autoImportCompletions": true,
#      "editor.formatOnSave": true,
#      "editor.codeActionsOnSave": {
#        "source.organizeImports": true,
#        "source.fixAll": true
#      },
#      "[python]": {
#        "editor.defaultFormatter": "charliermarsh.ruff"
#      },
#      "ruff.enable": true,
#      "ruff.lint.enable": true,
#      "ruff.format.args": ["--config", "pyproject.toml"]
#    }
#
# 8. GITHUB ACTIONS CI (optional):
#    Create .github/workflows/ci.yml:
#    name: CI
#    on: [push, pull_request]
#    jobs:
#      test:
#        runs-on: ubuntu-latest
#        steps:
#          - uses: actions/checkout@v4
#          - uses: astral-sh/setup-uv@v1
#          - run: uv pip install -e ".[dev]"
#          - run: uv run ruff format --check .
#          - run: uv run ruff check .
#          - run: uv run pyright
#          - run: uv run pytest
#
# ============================================================
# PYDANTIC V2 PATTERNS (IMPORTANT)
# ============================================================
#
# ✅ CORRECT (Pydantic v2):
# from pydantic import BaseModel, field_validator, model_validator, ConfigDict
#
# class User(BaseModel):
#     model_config = ConfigDict(strict=True)
#     name: str
#     age: int
#
#     @field_validator('age')
#     @classmethod
#     def validate_age(cls, v: int) -> int:
#         if v < 0:
#             raise ValueError('age must be positive')
#         return v
#
#     @model_validator(mode='after')
#     def validate_model(self) -> 'User':
#         return self
#
# ❌ WRONG (Pydantic v1 - deprecated):
# class User(BaseModel):
#     class Config:
#         strict = True
#
#     @validator('age')
#     def validate_age(cls, v):
#         return v
#
# ============================================================
# STRICTNESS LEVELS
# ============================================================
#
# This template is at MAXIMUM strictness. To reduce:
#
# LEVEL 1 - Production Ready (Recommended):
#   - Keep all current settings
#   - This is the gold standard
#
# LEVEL 2 - Slightly Relaxed:
#   - reportUnknownArgumentType = false
#   - reportUnknownVariableType = false
#   - reportUnknownMemberType = false
#   - reportUnused* = "warning" (instead of "error")
#
# LEVEL 3 - Gradual Adoption:
#   - typeCheckingMode = "standard"
#   - reportMissingSuperCall = false
#   - reportImplicitOverride = false
#
# ============================================================
# TROUBLESHOOTING
# ============================================================
#
# Q: Too many type errors from third-party libraries?
# A: Add to exclude list or set reportMissingTypeStubs = false
#
# Q: Pyright too slow?
# A: Add large directories to exclude list
#
# Q: Ruff "ALL" too strict?
# A: Replace "ALL" with specific rule codes (see template above)
#
# Q: Coverage failing?
# A: Reduce fail_under from 80 to 70 or 60
#
# Q: How to ignore specific errors temporarily?
# A: Use # type: ignore[error-code] or # noqa: RULE_CODE
#    But fix them eventually - strict mode means no ignores!
#