Skip to content
Back to skills

Python Typing And Async

ASecurity

Type hints are checked by a separate tool (`mypy`, `pyright`), never by CPython

  • 2 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 10, 2026
businesspythonexpress

Works with

  • cli

Security analysis

A100/100

Scanned September 10, 2026

npx -y skills add snoodleboot-io/prompticorn --skill python-typing-and-async --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Python Typing And Async?

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

Security grade badge for Python Typing And Async
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/snoodleboot-io-python-typing-and-async/badge)](https://www.skillsdirectory.com/skills/snoodleboot-io-python-typing-and-async)

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: python-typing-and-async
description: "Type hints are checked by a separate tool (`mypy`, `pyright`), never by CPython"
---

# Python Typing And Async (Verbose)

## Core Patterns

### Type The Boundaries

Type hints are checked by a separate tool (`mypy`, `pyright`), never by CPython
at runtime. Their value is proportional to how much a tool can prove. Annotate
public signatures, return types, and data structures; leave obvious locals bare.

```python
def group_by_role(users: list[User]) -> dict[str, list[User]]:
    out: dict[str, list[User]] = {}      # annotate: the checker can't infer empty
    for u in users:
        out.setdefault(u["role"], []).append(u)
    return out
```

Enable strict mode in CI. Without enforcement, hints drift out of sync with the
code and become misleading comments.

### Protocol vs ABC

Use a `Protocol` for a contract satisfied by shape, and an abstract base class
only when you also want to share implementation or force explicit registration.

```python
from typing import Protocol, runtime_checkable

@runtime_checkable
class Closeable(Protocol):
    def close(self) -> None: ...

def cleanup(res: Closeable) -> None:
    res.close()          # files, sockets, DB connections all satisfy this
```

A `Protocol` needs no cooperation from the classes it matches — third-party
types conform automatically if the methods line up. That is the point.

### TypedDict For Payload Shapes

```python
from typing import TypedDict, NotRequired

class Order(TypedDict):
    id: int
    total: float
    coupon: NotRequired[str]     # key may be absent (3.11+)

def summarize(o: Order) -> str:
    base = f"#{o['id']}: {o['total']}"
    return base + (f" ({o['coupon']})" if "coupon" in o else "")
```

`TypedDict` documents and checks a dict's keys without forcing a class or a
serialization step — ideal for JSON at the edges of a system.

### Literal And Narrowing

```python
from typing import Literal

Mode = Literal["r", "w", "a"]

def opener(mode: Mode) -> None:
    if mode == "r":
        ...              # inside this branch the checker knows mode == "r"
    else:
        reveal_type(mode)   # Literal["w", "a"]
```

Combine `Literal` with narrowing to make illegal states unrepresentable — a
typo like `"read"` fails the check at the call site.

### Generics And Bounds

```python
from typing import TypeVar

Num = TypeVar("Num", bound=float)   # int and float both count

def clamp(x: Num, lo: Num, hi: Num) -> Num:
    return max(lo, min(x, hi))
```

PEP 695 gives lighter syntax on 3.12+:

```python
def first[T](xs: list[T]) -> T | None:
    return xs[0] if xs else None
```

Generics keep the concrete type flowing through the call, so `first([1, 2])` is
`int | None`, not `object`.

### The Event Loop

`asyncio` runs a single-threaded event loop. `await` on an awaitable suspends the
current coroutine and hands control back to the loop, which runs another ready
task. Nothing is preemptive: a coroutine keeps the loop until it awaits.

```python
import asyncio

async def worker(name: str, delay: float) -> str:
    await asyncio.sleep(delay)     # suspension point — loop runs others here
    return f"{name} done"

async def main() -> None:
    async with asyncio.TaskGroup() as tg:
        for i in range(3):
            tg.create_task(worker(f"w{i}", i * 0.1))

asyncio.run(main())
```

### Keeping Blocking Work Off The Loop

A synchronous call has no suspension point, so it blocks the entire loop until it
returns. Offload it:

```python
import hashlib

# CPU-bound or legacy blocking I/O
digest = await asyncio.to_thread(hashlib.sha256, big_bytes)
```

`to_thread` runs the callable in a worker thread and awaits it — the loop stays
responsive. For CPU-bound work, threads still contend on the GIL; use a process
pool instead.

### gather vs TaskGroup

```python
# gather — ordered results, but a raised exception leaves siblings running
a, b = await asyncio.gather(fetch(1), fetch(2))

# TaskGroup — if any task fails, the rest are cancelled and errors surface together
async with asyncio.TaskGroup() as tg:
    t1 = tg.create_task(fetch(1))
    t2 = tg.create_task(fetch(2))
result = t1.result(), t2.result()
```

### The GIL And Choosing A Concurrency Model

The Global Interpreter Lock serializes execution of Python bytecode: one thread
at a time. This shapes the choice of tool:

| Workload | Tool | Why |
|---|---|---|
| Network / disk I/O | `asyncio` | Waiting releases the GIL; thousands of coroutines on one thread |
| Blocking library I/O | threads / `to_thread` | GIL released during the blocking syscall |
| Pure CPU computation | `multiprocessing` | Separate interpreters run truly in parallel |

Reaching for threads to accelerate a number-crunching loop is the classic
mistake — the GIL prevents the parallelism you were hoping for. See the
`python-runtime` skill for GIL internals and memory management.

## Common Anti-Patterns

❌ **`Any` to silence the checker**
```python
def load(path) -> Any:      # everything downstream is now unchecked
    return json.load(open(path))
```
✅ **Type the real shape**
```python
def load(path: str) -> dict[str, object]:
    with open(path) as f:
        return json.load(f)
```

❌ **Blocking calls inside a coroutine**
```python
async def get(url):
    return requests.get(url).json()   # freezes the loop for every caller
```
✅ **Use an async client, or offload**
```python
async def get(url):
    async with httpx.AsyncClient() as c:
        return (await c.get(url)).json()
```

❌ **Fire-and-forget tasks that swallow errors**
```python
asyncio.create_task(sync_to_remote())   # exception vanishes if never awaited
```
✅ **Own the task in a TaskGroup**
```python
async with asyncio.TaskGroup() as tg:
    tg.create_task(sync_to_remote())    # failure propagates
```

❌ **Threads for CPU-bound parallelism**
```python
with ThreadPoolExecutor() as ex:        # GIL serializes the work anyway
    ex.map(crunch, chunks)
```
✅ **Processes for CPU work**
```python
with ProcessPoolExecutor() as ex:
    ex.map(crunch, chunks)
```

## Typing And Async Checklist

- [ ] Public signatures and data shapes annotated; trivial locals left bare
- [ ] `mypy --strict` or `pyright` runs in CI, not just locally
- [ ] `Protocol` used for shape-based contracts instead of forced inheritance
- [ ] `TypedDict` / `Literal` express payload shapes and closed value sets
- [ ] Generics used so element types survive the call
- [ ] No `Any` added purely to mute the checker
- [ ] No blocking I/O, `time.sleep`, or CPU loops inside coroutines
- [ ] `asyncio.to_thread` / process pool for unavoidable blocking work
- [ ] `TaskGroup` preferred over bare `gather` for structured cancellation
- [ ] Concurrency model matches workload: async/threads for I/O, processes for CPU
- [ ] No un-awaited coroutines (watch the `RuntimeWarning`)

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…