agentleFS
mahdy-gribkovmahdy-gribkov/arcana/skills/python-best-practices/SKILL.md

python-best-practices

Skill1 starsChanged 8 months agoApache-2.0View on GitHub

Writes, reviews and scaffolds modern Python 3.14+ code with strict type hints, ruff, uv, src layout and pyproject.toml, async/await with TaskGroup, dataclasses vs Pydantic v2, and pytest conventions. Use when writing or reviewing a .py file, starting a Python project, choosing dataclass or Pydantic, configuring ruff, mypy or pyright, fixing async or blocking-IO bugs, or writing pytest tests.

  • Installs packages

19 other projects use a version of this.

Adopted by

Most recently taken up by

Used with

Projects with it also use react-best-practices, typescript-best-practices and 3 more.

Other files people use alongside this one.

Files found in the same public projects as a version of this one, more often than they turn up anywhere else. Each project counts once, large collections count for less, and this file's own repository is left out.
  • OpenAISkill

    react-best-practices

    React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.

    Also in 3 projects that use this

  • jaydubya818Skill

    typescript-best-practices

    Use when TypeScript needs focused type-safety, API, or error-handling guidance.

    Also in 3 projects that use this

  • BlockSkill

    frontend-design

    Create distinctive, production-grade frontend interfaces with high design quality. Use this skill when the user asks to build web components, pages, or applications. Generates creative, polished code that avoids generic AI aesthetics.

    Also in 4 projects that use this

  • SentrySkill

    skill-creator

    Create new agent skills following the Agent Skills specification. Use when asked to "create a skill", "add a new skill", "write a skill", "make a skill", "build a skill", or scaffold a new skill with SKILL.md. Guides through requirements, writing, registration, and verification.

    Also in 4 projects that use this

  • GoogleSkill

    tdd

    Test-driven development. Use when the user wants to build features or fix bugs test-first, mentions "red-green-refactor", or wants integration tests.

    Also in 3 projects that use this

The file

We split this skill into the rules it gives an agent

11 rules. 0 are common, 0 are distinctive, and 11 are very specific to this project.

  • 0

    Common

    Most of the 14 edited versions we read keep these.

  • 0

    Distinctive

    This author's own choices. Worth a look.

  • 11

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Never use Any unless interfacing with untyped third-party code.

    line 142
  2. Very specific:

    Use X | None not Optional[X] (3.10+ syntax).

    line 144
  3. Very specific:

    Use collections.abc types for parameters (Sequence, Mapping, Iterable), concrete types for return values (list, dict).

    line 145
  4. Very specific:

    Use httpx not requests for async HTTP.

    line 218
  5. Very specific:

    Never call blocking IO (open(), time.sleep(), requests calls) in async code.

    line 219
  6. Very specific:

    Use TaskGroup (3.11+) over raw gather for structured concurrency and better error handling.

    line 220
  7. Very specific:

    Bare except: Never except: or except Exception: without re-raising.

    line 277
  8. Very specific:

    String formatting with % or .format(): Use f-strings: f"Hello {name}".

    line 278
  9. Very specific:

    Nested dicts for structured data: Use dataclasses or Pydantic.

    line 280
  10. Very specific:

    os.path for path manipulation: Use pathlib.Path everywhere.

    line 281
  11. Very specific:

    print() for logging: Use logging module or structlog for structured logging.

    line 282

“Common” is measured: kept by at least 6 in 10 of the 14 edited versions of this file we could read. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

You are a Python expert specializing in modern Python 3.14+ with strict typing, fast tooling (ruff, uv), and production-grade patterns.

Use this skill when

  • Writing or reviewing Python code
  • Setting up Python project structure and tooling
  • Choosing between dataclasses, Pydantic, attrs
  • Building async Python services or CLI tools
  • Configuring linting, testing, or packaging

Project Structure (src layout)

myproject/
  pyproject.toml
  src/
    myproject/
      __init__.py
      main.py
      models.py
      services/
        __init__.py
        user.py
  tests/
    conftest.py
    test_models.py
    test_services/
      test_user.py

The src/ layout prevents accidental imports of the local package without installing it. Always use it for libraries. Flat layout is acceptable for single-file scripts and small apps only.

pyproject.toml (single source of truth)

[project]
name = "myproject"
version = "0.1.0"
requires-python = ">=3.14"
dependencies = [
    "httpx>=0.27",
    "pydantic>=2.6",
]

[dependency-groups]
dev = ["ruff", "pytest", "pytest-asyncio", "mypy", "pyright", "pre-commit"]

[project.scripts]
myproject = "myproject.main:cli"

[tool.ruff]
target-version = "py314"
line-length = 100
src = ["src"]

[tool.ruff.lint]
select = [
    "E", "F", "W",   # pyflakes + pycodestyle
    "I",              # isort
    "N",              # pep8-naming
    "UP",             # pyupgrade
    "B",              # flake8-bugbear
    "A",              # flake8-builtins
    "SIM",            # flake8-simplify
    "RUF",            # ruff-specific
    "ANN",            # flake8-annotations (type hint enforcement)
    "PT",             # flake8-pytest-style
]

[tool.ruff.lint.isort]
known-first-party = ["myproject"]

[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"

[tool.mypy]
strict = true
python_version = "3.14"

[tool.pyright]
pythonVersion = "3.14"
typeCheckingMode = "strict"
reportMissingTypeStubs = false

uv Package Manager

# Install uv (replaces pip, pip-tools, virtualenv, pipx)
# Use a package manager or the official installation page:
# https://docs.astral.sh/uv/getting-started/installation/
# macOS: brew install uv
# Windows: winget install astral-sh.uv
# Linux: pipx install uv

# Create project with virtual environment
uv init myproject && cd myproject
uv venv                     # creates .venv
uv add httpx pydantic       # adds to pyproject.toml + installs
uv add --dev ruff pytest    # dev dependencies
uv sync                     # install all deps from lockfile
uv run pytest               # run inside venv without activation
uv run ruff check src/      # lint
uv run mypy src/            # type check with mypy
uv run pyright src/         # type check with pyright (faster, VSCode default)

Why uv over pip: 10-100x faster, built-in lockfile (uv.lock), replaces 5 tools in one binary, written in Rust.

Type checker comparison:

  • mypy: Industry standard, slower, more mature plugins.
  • pyright: Faster, VS Code default, better error messages, strict mode catches more edge cases.

Type Hints (strict, everywhere)

from collections.abc import Awaitable, Callable, Mapping, Sequence

# Use builtin generics (3.9+), not typing.List/Dict
def process(items: list[str]) -> dict[str, int]: ...

# Use collections.abc for parameter types (accept more input types)
def find(items: Sequence[str], key: str) -> int | None: ...

# 3.12 type statement for aliases (replaces typing.TypeAlias)
type UserId = int
type Headers = Mapping[str, str]
type Point = tuple[float, float]
type Handler[T] = Callable[[T], Awaitable[None]]

# 3.12 generic syntax with a bound (replaces TypeVar)
def clone[T: Base](obj: T) -> T:
    return obj.model_copy()

Rules:

  • Never use Any unless interfacing with untyped third-party code. Even then, cast immediately.
  • Return types on every function. Parameter types on every parameter.
  • Use X | None not Optional[X] (3.10+ syntax).
  • Use collections.abc types for parameters (Sequence, Mapping, Iterable), concrete types for return values (list, dict).

Dataclasses vs Pydantic v2

# Dataclass: for internal data containers, no validation needed
from dataclasses import dataclass, field

@dataclass(frozen=True, slots=True)
class Point:
    x: float
    y: float
    label: str = ""
    tags: list[str] = field(default_factory=list)

# Pydantic v2: for external data (APIs, config, user input), validates on creation
from pydantic import BaseModel, Field, field_validator

class UserCreate(BaseModel):
    model_config = {"strict": True}

    name: str = Field(min_length=1, max_length=100)
    email: str
    age: int = Field(ge=0, le=150)

    @field_validator("email")
    @classmethod
    def validate_email(cls, v: str) -> str:
        if "@" not in v:
            raise ValueError("invalid email")
        return v.lower()

Decision rule: Pydantic for boundaries (API input, config files, external data). Dataclasses for everything internal. Never use plain dicts for structured data.

Async/Await Patterns

import asyncio
import httpx

# Concurrent HTTP requests
async def fetch_all(urls: list[str]) -> list[str]:
    async with httpx.AsyncClient(timeout=10.0) as client:
        tasks = [client.get(url) for url in urls]
        responses = await asyncio.gather(*tasks, return_exceptions=True)
        results: list[str] = []
        for resp in responses:
            if isinstance(resp, BaseException):
                results.append(f"ERROR: {resp}")
            else:
                results.append(resp.text)
        return results

# Semaphore for rate limiting
async def fetch_limited(urls: list[str], max_concurrent: int = 10) -> list[str]:
    sem = asyncio.Semaphore(max_concurrent)
    async with httpx.AsyncClient() as client:
        async def _get_text(url: str) -> str:
            async with sem:
                resp = await client.get(url)
                return resp.text
        return await asyncio.gather(*[_get_text(u) for u in urls])

# Structured concurrency with TaskGroup (3.11+)
async def process_batch(items: list[str]) -> None:
    async with asyncio.TaskGroup() as tg:
        for item in items:
            tg.create_task(process_one(item))
    # All tasks complete or all cancelled on first exception

Async rules:

  • Use httpx not requests for async HTTP. aiohttp is also fine.
  • Never call blocking IO (open(), time.sleep(), requests calls) in async code. Use asyncio.to_thread() for unavoidable blocking calls.
  • Use TaskGroup (3.11+) over raw gather for structured concurrency and better error handling.

Pytest Conventions

# tests/conftest.py: shared fixtures
from collections.abc import AsyncGenerator

import pytest
from myproject.db import Database

@pytest.fixture
async def db() -> AsyncGenerator[Database, None]:
    database = Database(":memory:")
    await database.connect()
    yield database
    await database.disconnect()

@pytest.fixture
def sample_user() -> dict[str, str]:
    return {"name": "Alice", "email": "alice@example.com"}

# tests/test_user.py
import pytest
from myproject.db import Database
from myproject.models import validate_email
from myproject.services.user import UserError, create_user

async def test_create_user_success(db: Database, sample_user: dict[str, str]) -> None:
    user = await create_user(db, **sample_user)
    assert user.name == "Alice"
    assert user.id is not None

async def test_create_user_duplicate_email(db: Database, sample_user: dict[str, str]) -> None:
    await create_user(db, **sample_user)
    with pytest.raises(UserError, match="already exists"):
        await create_user(db, **sample_user)

@pytest.mark.parametrize("email,valid", [
    ("user@example.com", True),
    ("invalid", False),
    ("", False),
    ("a@b.co", True),
])
def test_email_validation(email: str, valid: bool) -> None:
    if valid:
        assert validate_email(email) == email.lower()
    else:
        with pytest.raises(ValueError):
            validate_email(email)

Pytest rules: Name files test_*.py. Name functions test_*. Use fixtures, not setUp/tearDown. Use parametrize for data-driven tests. Use conftest.py for shared fixtures (pytest discovers them automatically).

Anti-Patterns to Avoid

  1. Mutable default arguments: def f(items=[]) shares the list across calls. Use def f(items: list[str] | None = None) then items = items or [].
  2. Bare except: Never except: or except Exception: without re-raising. Catch specific exceptions.
  3. String formatting with % or .format(): Use f-strings: f"Hello {name}".
  4. import *: Never in production code. Pollutes namespace, breaks type checkers.
  5. Nested dicts for structured data: Use dataclasses or Pydantic. data["user"]["address"]["city"] is untyped and crashes with KeyError.
  6. os.path for path manipulation: Use pathlib.Path everywhere.
  7. print() for logging: Use logging module or structlog for structured logging.
  8. Not using if __name__ == "__main__": in executable modules.
  9. Global mutable state: Pass dependencies as parameters. Use dependency injection.
  10. Ignoring ruff/mypy warnings: Fix them. They catch real bugs.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.