Skip to content
Back to skills

Modern Python Development

ASecurity

Implements modern Python 3.10+ development practices including structural

  • 4 stars
  • 0 votes
  • 0 copies
  • 4 views
  • Added June 12, 2026
documentationpythongobashexpresstestingrefactoringapibackendperformancedocumentation

Works with

  • cli
  • api

Security analysis

A100/100

Scanned June 12, 2026

npx -y skills add paulpas/agent-skill-router --skill modern-python-development --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Modern Python Development?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Modern Python Development
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/paulpas-modern-python-development/badge)](https://www.skillsdirectory.com/skills/paulpas-modern-python-development)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---




name: modern-python-development
description: Implements modern Python 3.10+ development practices including structural
  union types, TypeAlias, Self, ParamSpec, TaskGroup structured concurrency, httpx
  async patterns, and pyproject.toml-based project structure with uv.
license: MIT
compatibility: opencode
metadata:
  version: "1.0.0"
  domain: coding
  triggers: python typing, python 3.12, python 3.11, TypeAlias, ParamSpec, asyncio TaskGroup, structured concurrency, pyproject.toml TaskGroup
  archetypes:
  - tactical
  - generation
  anti_triggers:
  - brainstorming
  - vague ideation
  - code golf
  - over-engineering
  response_profile:
    verbosity: low
    directive_strength: high
    abstraction_level: operational
  role: implementation
  scope: implementation
  output-format: code
  content-types:
  - code
  - guidance
  - do-dont
  - examples
  related-skills: python-testing-strategies, go-concurrency-patterns, python-package-publishing




---




# Modern Python Development (3.10+)

Implements modern Python development practices for Python 3.10 through 3.13+, covering structural union types, explicit type aliases, fluent interface patterns with Self, decorator signature preservation with ParamSpec, TaskGroup structured concurrency, httpx async I/O, and pyproject.toml-based project structure with uv as the package manager.

## TL;DR Checklist

- [ ] Use `X | Y` for unions instead of `typing.Union[X, Y]`
- [ ] Use `TypeAlias` annotation for all type aliases — never bare assignment
- [ ] Use `Self` (PEP 673) for fluent interface method return types
- [ ] Use `@override` (PEP 698) on every class method override
- [ ] Use `ParamSpec` + `Concatenate` to preserve function signatures through decorators
- [ ] Replace `asyncio.create_task()` fire-and-forget with `asyncio.TaskGroup`
- [ ] Wrap all timeout logic in `asyncio.timeout()` context manager — never `wait_for()`
- [ ] Offload blocking I/O with `asyncio.to_thread()` — never run sync code on the event loop
- [ ] Use httpx for async HTTP — never requests inside async functions
- [ ] Structure projects with single pyproject.toml and uv (no requirements.txt, no setup.py)

---

## When to Use

Use this skill when:

- Writing new Python code targeting 3.10+ and wanting modern type annotations
- Refactoring legacy Python code that uses `Union[]`, `Optional[]`, or bare type alias assignments
- Implementing async functions with I/O — choosing between TaskGroup, gather(), or fire-and-forget tasks
- Setting up a new Python project — choosing pyproject.toml structure, build backend, and package manager
- Writing decorators that wrap functions and need to preserve the wrapped function's signature for type checkers
- Implementing fluent/chainable APIs with methods returning `self`

---

## When NOT to Use

Avoid this skill for:

- Maintaining Python 3.9 or earlier codebases (these features require 3.10+ runtime)
- Performance-critical numerical computing — use NumPy/NumPy-like patterns instead of typing-focused guidance
- Project setup for teams committed to Poetry or pipenv — the uv + pyproject.toml approach is explicitly different

---

## Core Workflow

### Part A: Typing (3.10+)

1. **Assess type annotation needs** — For every public function, determine if parameters and return values benefit from explicit types. Private/internal functions without external callers can skip annotations unless they are complex or error-prone.

   **Checkpoint:** Every function signature exposed to other modules must have typed parameters and return values.

2. **Apply structural union syntax** — Replace `typing.Union[X, Y]` with `X | Y` and `typing.Optional[X]` with `X | None`. Use `from __future__ import annotations` only when circular references require deferred evaluation of type expressions.

   ```python
   # ❌ BAD — legacy typing module usage (Python 3.9 and earlier)
   from typing import Union, Optional, List, Dict

   def process(
       items: Union[str, bytes],
       metadata: Optional[Dict[str, Union[int, str]]] = None
   ) -> List[str]:
       ...
   ```

3. **Apply explicit TypeAlias for named types** — Every type alias must use the `TypeAlias` annotation from `typing`. Bare assignment creates a structurally equivalent name that loses identity across all checkers.

   ```python
   # ❌ BAD — bare assignment loses type identity; mypy and pyright treat this as dict[str, Any]
   JsonPayload = dict[str, Any]
   UserId = int

   # ✅ GOOD — explicit TypeAlias preserves distinct identity for type checkers
   from typing import TypeAlias

   JsonPayload: TypeAlias = dict[str, Any]
   UserId: TypeAlias = int
   ```

4. **Apply Self for fluent interfaces** — Methods returning `self` must use `typing.Self` (PEP 673) instead of string-quoted class names. This preserves the actual subclass type through method chains.

   ```python
   # ❌ BAD — returns the declaring class, losing subclass identity through chains
   from typing import TYPE_CHECKING

   if TYPE_CHECKING:
       pass

   class QueryBuilder:
       def where(self, field: str, value: str) -> "QueryBuilder":
           return self

       def order_by(self, field: str) -> "QueryBuilder":
           return self

   # ❌ BAD — string-quoted name is fragile and breaks with complex inheritance

   # ✅ GOOD — Self preserves actual class type through chains
   from typing import Self

   class QueryBuilder:
       def where(self, field: str, value: str) -> Self:
           return self

       def order_by(self, field: str) -> Self:
           return self


   class AdvancedQueryBuilder(QueryBuilder):
       def filter(self, condition: str) -> Self:
           return self


   # Type checker knows the full chain returns AdvancedQueryBuilder
   qb = AdvancedQueryBuilder().where("name", "test").order_by("name").filter("active")
   reveal_type(qb)  # AdvancedQueryBuilder (not QueryBuilder)
   ```

5. **Apply ParamSpec and Concatenate for decorators** — Decorators must preserve the wrapped function's signature using `ParamSpec` for argument capture and `Concatenate` when injecting locked parameters.

   ```python
   # ❌ BAD — decorator completely loses function signature; type checker sees Callable[..., Any]
   import functools
   from typing import Any, Callable

   def authenticate(f: Callable[..., Any]) -> Callable[..., Any]:
       @functools.wraps(f)
       async def wrapper(*args: Any, **kwargs: Any) -> Any:
           check_auth()
           return await f(*args, **kwargs)
       return wrapper


   # ✅ GOOD — ParamSpec preserves exact parameter types through the decorator layer
   from typing import ParamSpec, TypeVar

   P = ParamSpec("P")
   R = TypeVar("R")

   def authenticate(f: Callable[P, R]) -> Callable[P, R]:
       @functools.wraps(f)
       async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
           check_auth()
           return await f(*args, **kwargs)
       return wrapper

   # Type checker knows: @decorate(async def fetch_user(id: int) -> User: ...)
   # still has signature (id: int) -> User


   # ✅ GOOD — Concatenate locks a parameter at the front (e.g., injected dependency)
   from typing import Concatenate

   DB = object  # simplified for example

   def with_db(f: Callable[Concatenate[DB, P], R]) -> Callable[P, R]:
       @functools.wraps(f)
       async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
           db = get_db()  # injected dependency
           return await db(db, *args, **kwargs)
       return wrapper

   @with_db
   async def fetch_user(db: DB, user_id: int) -> User:
       ...

   # Caller passes only (user_id=42); type checker infers the DB parameter automatically
   ```

6. **Apply @override on every method override** — Every method that overrides a parent class method must use `@override` (PEP 698, Python 3.12+) to catch renames and misspellings at static analysis time.

   ```python
   # ❌ BAD — silent bug if parent method is renamed or subclass misspells
   class HTTPClient(BaseHTTPClient):
       def send_request(self, req: Request) -> Response:  # typo in name? No error.
           ...


   # ✅ GOOD — type checker catches override errors immediately
   from typing import override

   class HTTPClient(BaseHTTPClient):
       @override
       def send_request(self, req: Request) -> Response:
           return self._dispatch(req)

       @override
       def close(self) -> None:
           ...
   ```

### Part B: Async Patterns (3.11+)

7. **Use TaskGroup for structured concurrency** — Replace `asyncio.create_task()` fire-and-forget patterns with `asyncio.TaskGroup` (Python 3.11+). Every spawned task must have a defined scope and lifecycle.

   ```python
   # ❌ BAD — fire-and-forget creates orphaned tasks; exceptions are lost or raise at cleanup
   async def fetch_all_bad(urls: list[str]) -> dict[str, Any]:
       results = {}
       for url in urls:
           task = asyncio.create_task(_fetch_json(url))  # No tracking!
           results[url] = await task  # Exceptions from earlier tasks mask later ones

       return results


   # ✅ GOOD — TaskGroup provides structured scope with guaranteed cleanup
   import asyncio
   import httpx

   async def fetch_all(urls: list[str]) -> dict[str, Any]:
       async with asyncio.TaskGroup() as tg:
           tasks: dict[str, asyncio.Task] = {
               url: tg.create_task(_fetch_json(url)) for url in urls
           }
       # All tasks completed (success or failure). ExceptionGroup raised if any failed.
       return {url: task.result() for url, task in tasks.items()}


   # ✅ GOOD — handle multiple error types with except* syntax (Python 3.11+)
   async def fetch_all_with_error_handling(urls: list[str]) -> dict[str, Any]:
       try:
           async with asyncio.TaskGroup() as tg:
               tasks: dict[str, asyncio.Task] = {
                   url: tg.create_task(_fetch_json(url)) for url in urls
               }
       except* ValueError as eg:
           # Handle each validation error separately
           for exc in eg.exceptions:
               log_validation_error(exc)
           raise RuntimeError("Some requests failed validation") from eg
       except* ConnectionError as eg:
           # Retry failed connections only
           retryable = [exc for exc in eg.exceptions]
           await retry_connections(retryable)
           raise RuntimeError(f"{len(retryable)} connections failed after retry") from eg

       return {url: task.result() for url, task in tasks.items()}
   ```

8. **Use asyncio.timeout() instead of wait_for()** — Replace `asyncio.wait_for()` with the `asyncio.timeout()` context manager (Python 3.11+). The timeout raises plain `TimeoutError`, not asyncio-specific exceptions.

   ```python
   # ❌ BAD — wait_for() is deprecated; mixes cancellation and timeout semantics
   import asyncio

   async def fetch_with_timeout_bad(url: str) -> Response:
       try:
           result = await asyncio.wait_for(fetch_data(url), timeout=5.0)
           return result
       except asyncio.TimeoutError:
           handle_error()


   # ✅ GOOD — explicit context manager with clean cancellation propagation
   async def fetch_with_timeout(url: str) -> Response:
       try:
           async with asyncio.timeout(5.0):
               return await fetch_data(url)
       except TimeoutError:  # Plain built-in TimeoutError, not asyncio-specific
           handle_error()
   ```

9. **Offload blocking I/O with asyncio.to_thread()** — Never run synchronous blocking code directly in an async function body. Use `asyncio.to_thread()` to schedule it on the thread pool.

   ```python
   # ❌ BAD — blocks the entire event loop; no other coroutines can make progress
   def heavy_computation(data: bytes) -> str:
       return expensive_processing(data)  # Blocks event loop for potentially seconds

   async def process_request_bad(data: bytes):
       result = heavy_computation(data)  # BUG: blocks everything
       return format_response(result)


   # ✅ GOOD — offloads to thread pool; event loop remains responsive
   import asyncio

   async def process_request(data: bytes):
       result = await asyncio.to_thread(heavy_computation, data)
       return format_response(result)
   ```

10. **Use httpx for all async HTTP** — Never use `requests` inside async code. Use `httpx.AsyncClient` with connection pooling and proper lifetime management.

    ```python
    # ❌ BAD — requests is synchronous; wrapping in to_thread is awkward and sequential
    import requests

    async def fetch_multiple_bad(urls: list[str]) -> list[dict]:
        results = []
        for url in urls:
            data = await asyncio.to_thread(requests.get, url).json()
            results.append(data)  # Sequential — slow, one at a time
        return results


    # ✅ GOOD — native async with connection pooling and parallel execution
    import httpx

    async def fetch_multiple(urls: list[str]) -> list[dict]:
        async with httpx.AsyncClient(timeout=10.0) as client:
            responses = await asyncio.gather(
                *(client.get(url) for url in urls),
                return_exceptions=True
            )
        return [r.json() for r in responses if isinstance(r, httpx.Response)]

    # Note: always use async with to ensure AsyncClient is properly closed
    # Always set explicit timeout to prevent indefinite hangs
    ```

### Part C: Project Structure (pyproject.toml + uv)

11. **Create pyproject.toml as the single configuration file** — Eliminate requirements.txt, setup.py, and setup.cfg. Everything goes in pyproject.toml with a modern build backend.

    ```toml
    # ✅ GOOD — minimal but complete pyproject.toml (PEP 621 compliant)
    [project]
    name = "my-project"
    version = "0.1.0"
    description = "A concise one-line description of what this project does"
    readme = "README.md"
    requires-python = ">=3.12"
    dependencies = [
        "httpx>=0.28",
        "pydantic>=2.10",
    ]

    [project.optional-dependencies]
    dev = [
        "pytest>=8.3",
        "pytest-asyncio>=0.25",
        "ruff>=0.9",
        "mypy>=1.14",
    ]

    [project.scripts]
    my-cli = "my_project.cli:main"

    [build-system]
    requires = ["hatchling"]
    build-backend = "hatchling.build"

    [tool.uv]
    dev-dependencies = [
        "pytest>=8.3",
        "ruff>=0.9",
        "mypy>=1.14",
    ]

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

    [tool.ruff.lint]
    select = ["E", "F", "I", "UP", "B", "SIM", "RUF"]

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

    [tool.mypy]
    python_version = "3.12"
    strict = true
    warn_return_any = true
    ```

12. **Choose src/ layout for libraries, flat for scripts** — Libraries must use `src/` layout to prevent importing from the source root in development (which masks packaging issues). Scripts can use flat layout for simplicity.

    ```
    # Library project — src layout (MANDATORY for packages)
    my_library/
    ├── pyproject.toml
    ├── README.md
    ├── uv.lock              # committed lockfile for reproducibility
    ├── src/
    │   └── my_library/
    │       ├── __init__.py
    │       ├── core.py      # main module
    │       └── cli.py       # CLI entry point
    └── tests/
        ├── __init__.py
        └── test_core.py

    # Script project — flat layout (acceptable for single-file or small tools)
    my_script/
    ├── pyproject.toml
    ├── main.py
    └── uv.lock
    ```

13. **Use uv commands as the single interface** — Never manually manage virtualenvs. Use `uv add` to install, `uv run` to execute, and `uv sync` to resolve. Always commit `uv.lock`.

    ```bash
    # Create a new project (auto-generates pyproject.toml + src layout)
    uv init my-project
    cd my-project

    # Add production dependencies (instant resolution, updates lockfile)
    uv add httpx pydantic

    # Add development dependencies
    uv add --dev pytest ruff mypy

    # Run code with correct environment — no venv activation needed
    uv run python -m my_project.cli
    uv run pytest
    uv run ruff check .
    uv run mypy src/

    # Sync all dependencies from lockfile (deterministic)
    uv sync

    # Update all packages to newest compatible versions
    uv lock --upgrade
    ```

---

## Constraints

### MUST DO
- Use `X | Y` union syntax instead of `typing.Union[X, Y]` for all Python 3.10+ code
- Annotate every type alias with `TypeAlias` — never use bare assignment for named types
- Use `Self` (PEP 673) for fluent interface method return types — never string-quoted class names
- Apply `@override` (PEP 698) on every method that overrides a parent class method
- Preserve function signatures through decorators using `ParamSpec` and `Concatenate`
- Use `asyncio.TaskGroup()` for all multi-task spawning — never bare `create_task()` outside context
- Wrap async I/O in `asyncio.timeout()` with explicit timeout values — every I/O operation needs one
- Offload blocking synchronous code with `asyncio.to_thread()` — never run sync I/O on the event loop
- Use httpx.AsyncClient for all async HTTP — never requests or aiohttp inside async functions
- Structure all libraries with src/ layout and single pyproject.toml — no requirements.txt, no setup.py
- Commit the lockfile (`uv.lock` or `poetry.lock`) for deterministic builds
- Pin `requires-python` to a minimum version in pyproject.toml (e.g., `>=3.12`)

### MUST NOT DO
- Use bare type alias assignment (`JsonType = dict[str, Any]`) — always use `TypeAlias` annotation
- Use string-quoted forward references for return types of methods returning self — use `Self`
- Ignore errors from `asyncio.create_task()` outside a TaskGroup context — orphaned tasks lose exceptions
- Mix blocking I/O (requests, sync DB drivers, open()/read()) directly in async functions — always use to_thread or async alternatives
- Run `sync` HTTP clients inside event loops without offloading — this blocks all concurrent operations
- Manually create and activate virtualenvs — uv manages environments automatically via `uv run`
- Use `asyncio.wait_for()` for timeouts — it is deprecated in favor of `asyncio.timeout()` context manager
- Omit `@override` on subclass methods — misspelled method names or parent renames cause silent bugs
- Allow bare `Any` types in public API signatures — prefer specific types or structural protocols
- Commit projects without a lockfile — nondeterministic builds break CI and production deployments

---

## Output Template

When implementing or reviewing Python code with this skill active, produce:

1. **Typing Audit** — List all functions requiring modern type annotations, specifying union syntax, TypeAlias usage, Self returns, ParamSpec decorators, and @override placements
2. **Async Pattern Review** — Identify all async I/O operations and specify: TaskGroup scope, timeout duration, httpx vs sync client, to_thread offload points
3. **Project Structure Assessment** — Confirm pyproject.toml completeness, src/ layout adherence, uv commands used, lockfile presence
4. **Migration Plan** — For legacy code: exact line-by-line changes needed to upgrade from Union[]/Optional[] to | syntax, add TypeAlias annotations, replace fire-and-forget tasks with TaskGroup

---

## Related Skills

| Skill | Purpose |
|---|---|
| `python-testing-strategies` | Pytest fixtures, parametrize, mocking, coverage — complements modern Python project structure |
| `go-concurrency-patterns` | Worker pools and context cancellation — Go alternative patterns for comparison when evaluating concurrency approaches |

---

## Live References

> Authoritative documentation links for this skill's domain. The model follows markdown links at load time to resolve external references and inline content.

- [Python 3.12 Documentation — The typing Module](https://docs.python.org/3/library/typing.html)
- [PEP 604 — Union Operator `X | Y`](https://peps.python.org/pep-0604/)
- [PEP 673 — Self Type for Methods Returning Self](https://peps.python.org/pep-0673/)
- [PEP 612 — ParamSpec and Concatenate for Decorators](https://peps.python.org/pep-0612/)
- [PEP 698 — @override Decorator](https://peps.python.org/pep-0698/)
- [asyncio TaskGroup Documentation](https://docs.python.org/3/library/asyncio-task.html#asyncio.TaskGroup)
- [uv Documentation — Package Manager for Python](https://docs.astral.sh/uv/)

---

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.