Maximum strictness for Python projects, inspired by TypeScript’s --strict mode.
This repo provides a pyproject.toml template and tooling configuration so you can:
- Start new projects in strict mode by copying the template.
- Upgrade existing projects by reusing only the pieces you need.
The goal is production‑grade quality from day one: strong typing, clean style, security checks, and good test coverage.
- Python: 3.12+
- Dependency manager:
uv - Type checker:
basedpyright(strict mode) - Linter / formatter:
ruff(with many plugins enabled) - Config & env:
python-dotenv,typing-extensions - Data validation:
pydantic>=2 - Task runner:
poethepoet(format, check, metrics, etc.) - Code quality tools:
radon,skylos - Testing & coverage:
pytest+coverage(80% minimum by default)
Everything is wired through pyproject.toml so you have a single source of truth.
-
Create a new directory:
mkdir my-project cd my-project -
Copy the template
pyproject.tomlfrom this repo and adapt it:-
Change
[project]fields:name = "my-project"description = "My strict Python project"authors = [...]
-
Update
tool.ruff.lint.isort.known-first-partyto match your package name, e.g.:known-first-party = ["my_project"]
-
-
Create the basic structure:
mkdir -p src/my_project tests touch src/my_project/__init__.py touch tests/__init__.py
-
Create and activate a virtual environment (with
uv):uv venv # Windows .venv\Scripts\activate # Linux / macOS # source .venv/bin/activate
-
Install dependencies (including dev tooling):
uv pip install -e ".[dev]"
You now have a new project in strict mode.
You don’t have to adopt everything at once. For an existing codebase you can gradually copy pieces of this template:
-
Back up your current
pyproject.toml(orsetup.cfg,requirements.txt, etc.). -
Copy selected sections from this template
pyproject.toml:[project](or integrate relevant fields into your existing project metadata).[project.optional-dependencies].dev(to addbasedpyright,ruff,poethepoet, etc.).[tool.ruff]and[tool.ruff.*]blocks to adopt strict linting/formatting.[tool.basedpyright...]block to enable strict type checking.[tool.pytest.ini_options]and[tool.coverage.*]to standardize tests/coverage.
-
Simplify if needed:
- You can relax some rules initially (e.g. unused variables, unknown types) and then tighten them once the code is cleaner.
- You can exclude noisy directories (generated code, legacy folders) in the
excludelists forruff,basedpyright, and coverage.
-
Install the dev dependencies in your existing project:
uv pip install -e ".[dev]" -
Run the tools and fix issues iteratively (see commands below).
You can use the raw tools or the poethepoet tasks defined in pyproject.toml.
# Format code
uv run ruff format .
# Lint and auto-fix
uv run ruff check . --fix
# Strict type checking
uv run basedpyright --log-level error
# Run tests with coverage
uv run pytest# Format, lint (with auto-fix), and strict type check
uv run poe format
# Lint + strict type check (no auto-fix)
uv run poe check
# Lint only (with auto-fix)
uv run poe lint
# Lint with unsafe fixes
uv run poe lint-unsafe
# Quality metrics (vulture + radon)
uv run poe metrics
# Full quality pipeline (format + lint + type check + metrics)
uv run poe qualityThese tasks are configured in [tool.poe.tasks] in pyproject.toml and you can customize them as needed.
This template targets maximum strictness, roughly equivalent to:
- TypeScript
--strict - Python with
basedpyrightinstrictmode - Aggressive linting via
ruffwith many plugins enabled
That means:
- No implicit
Any(unknown/untyped values are treated as errors). - Required type annotations on function parameters and return types.
- Unused imports, variables, functions, and classes are treated as errors.
- Optional values (
None) must be handled explicitly. - Security‑oriented checks via
flake8-banditrules. - Coverage threshold enforced by default (80%).
If this is too strict for an existing project, you can progressively relax some basedpyright or ruff rules and then turn them back on as your code improves.
This template is specifically designed to prevent common "LLM slop" - the anti-patterns that AI code assistants tend to produce. Here are key patterns to follow:
Never work with raw dictionaries, JSON, or untyped data. Always define a Pydantic model and validate immediately at the boundary:
# ❌ BAD: Raw dict from API/file
def process_user_data(data: dict) -> None:
name = data["name"] # Could fail, no validation
age = data.get("age", 0) # Type is Any
# ✅ GOOD: Pydantic model + immediate validation
from pydantic import BaseModel, Field
class UserData(BaseModel):
name: str = Field(min_length=1)
age: int = Field(ge=0, le=150)
def process_user_data(data: dict) -> None:
user = UserData.model_validate(data) # Fails fast with clear errors
# Now user.name and user.age are fully typed and validatedThis applies to:
- API responses:
response.json()→ immediateModel.model_validate() - Config files:
json.load()→ immediateModel.model_validate() - CLI arguments:
argparse.Namespace→ convert to Pydantic model - Environment variables: Use
pydantic-settingsinstead of rawos.getenv()
If you work with DataFrames, uncomment Pandera in dependencies and always define schemas:
import pandera as pa
from pandera.typing import DataFrame
class SalesSchema(pa.DataFrameModel):
product_id: int = pa.Field(gt=0)
revenue: float = pa.Field(ge=0)
date: pa.DateTime
def process_sales(df: DataFrame[SalesSchema]) -> DataFrame[SalesSchema]:
validated = SalesSchema.validate(df, lazy=True)
# Now every column is typed and validated
return validatedThe FBT rule catches this common LLM mistake:
# ❌ BAD: What does True mean here?
send_email(user, True, False)
# ✅ GOOD: Use enums or keyword-only args
from enum import Enum
class EmailFormat(Enum):
HTML = "html"
PLAIN = "plain"
def send_email(
user: User,
*, # Force keyword-only
format: EmailFormat,
async_send: bool = False
) -> None: ...
send_email(user, format=EmailFormat.HTML, async_send=False)This template enforces:
- Max complexity: 10 (McCabe)
- Max nested blocks: 3 (no deeply nested if/for/while)
If you hit these limits, refactor into smaller functions:
# ❌ BAD: Complex nested logic
def process_order(order):
if order.valid:
if order.in_stock:
if order.payment_ok:
if order.address_valid:
# 4 levels deep!
...
# ✅ GOOD: Early returns + helper functions
def process_order(order: Order) -> None:
if not order.valid:
raise InvalidOrderError()
if not order.in_stock:
raise OutOfStockError()
validate_payment(order)
validate_address(order)
ship_order(order)LLMs love except Exception: or bare except:. The BLE rule catches this:
# ❌ BAD: Hides all errors
try:
risky_operation()
except Exception: # BLE001 error
pass
# ✅ GOOD: Catch specific exceptions
try:
risky_operation()
except (ValueError, KeyError) as e:
logger.error(f"Expected error: {e}")
raiseIn a project using this template, a typical .vscode/settings.json would:
- Point VS Code to
.venvas the default interpreter. - Enable strict type checking.
- Use
ruffas formatter and linter. - Run organize‑imports and fix‑all on save.
You can copy the suggested settings from the original template comments or adapt them to your editor of choice.
- Greenfield projects where you want high confidence and maintainability.
- Existing projects that you want to gradually migrate to a stricter, safer style.
- Libraries/SDKs that will be consumed by others and must provide strong type guarantees.
If you want to relax the rules (e.g., for experimental code or quick scripts), you can:
- Lower coverage thresholds.
- Downgrade some
basedpyrightdiagnostics from errors to warnings. - Disable specific
ruffrules or directories.
- Adjust
[project]metadata to match your package. - Update
known-first-partyin theruffconfig. - Add your source code under
src/and tests undertests/. - Run
uv run poe qualitybefore every commit or CI run.
This template is designed so you can copy it wholesale for new projects or cherry‑pick sections for older ones, while keeping the same strict philosophy throughout your Python codebase.