Skip to content
Back to skills

2521 Typing Strategy 31adbe37

ASecurity

Guide for choosing Protocol, TypeVar, ParamSpec, or concrete types in stdlib-only scripts

  • 9 stars
  • 0 votes
  • 0 copies
  • 0 views
  • Added October 11, 2026
businesspythonbashapi

Works with

  • api

Security analysis

A100/100

Scanned October 11, 2026

npx -y skills add tools-only/X-Skills --skill 2521-typing-strategy_31adbe37 --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of 2521 Typing Strategy 31adbe37?

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

Security grade badge for 2521 Typing Strategy 31adbe37
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tools-only-2521-typing-strategy-31adbe37/badge)](https://www.skillsdirectory.com/skills/tools-only-2521-typing-strategy-31adbe37)

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

SKILL.md
---
title: Typing Strategy Reference
description: Guide for choosing Protocol, TypeVar, ParamSpec, or concrete types in stdlib-only scripts
version: 1.0.0
last_updated: '2026-02-15'
document_type: reference
python_compatibility: 3.11+
---

# Typing Strategy Reference

Comprehensive guide for type hint patterns in stdlib-only Python scripts.

## Choose Protocol, TypeVar, ParamSpec, or Concrete Types

### 1. Prefer Protocol When Contract is Behavioral

Use when you only need operations, not identity.

**Examples:** file-like, writer/reader, closable, path-provider

```python
from typing import Protocol

class Reader(Protocol):
    def read(self, n: int = -1) -> bytes: ...

class Writer(Protocol):
    def write(self, data: bytes) -> int: ...

def copy(src: Reader, dst: Writer) -> int:
    buf = src.read()
    return dst.write(buf)
```

### 2. Prefer TypeVar When Output Type Relates to Input Type

Use for containers and transformers that preserve or map element types.

```python
from typing import TypeVar, Iterable, Callable

T = TypeVar("T")
U = TypeVar("U")

def transform(xs: Iterable[T], f: Callable[[T], U]) -> list[U]:
    return [f(x) for x in xs]
```

### 3. Prefer ParamSpec for Higher-Order Callables

Forward parameter lists through decorators and wrappers.

```python
from typing import Callable, ParamSpec, TypeVar

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

def traced(fn: Callable[P, R]) -> Callable[P, R]:
    def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
        return fn(*args, **kwargs)
    return wrapper
```

### 4. Prefer Abstract Collections for API Flexibility

- `Iterable[T]` for simple iteration
- `Sequence[T]` for ordered, indexable, sized inputs
- `Mapping[K, V]` for read-only dict-like
- Use concrete `list[T]` or `dict[K, V]` only when you require that implementation

```python
from collections.abc import Sequence, Mapping

def first(xs: Sequence[str]) -> str | None:
    return xs[0] if xs else None

def keys(m: Mapping[str, int]) -> list[str]:
    return list(m.keys())
```

### 5. Prefer `object` Over `Any` When Type Unknown

`object` forces explicit narrowing at use sites.

`Any` is allowed at dynamic boundaries, but keep its scope tight and eliminate it quickly.

## Guidance on Any

- Accept `Any` only at boundaries (JSON, YAML, untyped plugins)
- Narrow immediately using runtime checks, TypedDict, dataclasses, or validators
- Do not expose `Any` in public APIs
- Prefer aliases that communicate shape (e.g., `Json`)

**Recommended alias for clarity:**

```python
from typing import Any, TypeAlias

Json: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None
```

**Narrowing pattern:**

```python
import json
from pathlib import Path
from typing import Any

def load_json(path: Path) -> Json:
    return json.loads(path.read_text())

def load_object(path: Path) -> dict[str, Any]:
    data: Any = load_json(path)
    if not isinstance(data, dict):
        raise ValueError("Expected top-level JSON object")
    return data
```

## Overloads vs Unions vs Protocols

- **Overloads:** Return type depends on distinct input shapes and you want precise call-site inference
- **Unions:** Multiple shapes accepted and treated uniformly
- **Protocols:** Behavior drives compatibility more than shape

## Type Aliases: Path-Like and Command Vectors

Use concise aliases to make signatures readable and correct across platforms.

```python
import os
from typing import TypeAlias
from collections.abc import Sequence

StrPath: TypeAlias = str | os.PathLike[str]
BytesPath: TypeAlias = bytes | os.PathLike[bytes]
Pathish: TypeAlias = StrPath | BytesPath
Cmd: TypeAlias = Sequence[Pathish]
```

**Rules:**

- Prefer `Sequence` over `list` in APIs that only read arguments
- Use `StrPath` in most file APIs; only use `BytesPath` when you truly handle bytes paths
- Convert to `str` at execution boundaries: `str(p)` for logging and subprocess

## Protocols for File-Like and Path-Providing Objects

**File-like inputs:**

```python
from typing import Protocol

class Readable(Protocol):
    def read(self, n: int = -1) -> bytes: ...

def digest(stream: Readable) -> bytes:
    data = stream.read()
    # compute hash...
    return data
```

**Path-providing inputs:**

```python
from typing import Protocol
import os

class HasPath(Protocol):
    def __fspath__(self) -> str | bytes: ...

def open_text(x: HasPath) -> str:
    with open(os.fspath(x), "rt", encoding="utf-8") as fh:
        return fh.read()
```

**Why Protocol:**

- You want behavior (read, `__fspath__`) without binding to concrete classes
- Extensible: works with pathlib objects, custom wrappers, or in-memory fakes

## Choosing Between Sequence vs List and Mapping vs Dict

- Accept `Sequence[T]` when you only iterate or index and do not mutate
- Accept `Mapping[K, V]` when you only read by key
- Return concrete `list[T]`/`dict[K, V]` if you construct new containers and callers can rely on mutability

```python
from collections.abc import Sequence, Mapping

def first(xs: Sequence[str]) -> str | None:
    return xs[0] if xs else None

def keys(m: Mapping[str, int]) -> list[str]:
    return list(m.keys())
```

## Overloads for Precise APIs

Use overloads when output type depends on input flags. Keep implementation single and type-safe.

```python
from typing import overload

@overload
def parse_int(s: str, *, strict: bool) -> int: ...
@overload
def parse_int(s: str, *, strict: bool = ...) -> int | None: ...

def parse_int(s: str, *, strict: bool = False) -> int | None:
    try:
        return int(s)
    except ValueError:
        if strict:
            raise
        return None
```

## JSON Boundary Standard

- Alias `Json` for clarity
- Parse, then narrow quickly to precise models using TypedDict or dataclasses

```python
from typing import Any, TypedDict, TypeAlias

Json: TypeAlias = dict[str, Any] | list[Any] | str | int | float | bool | None

class AppConfig(TypedDict):
    host: str
    port: int

def to_app_config(j: Json) -> AppConfig:
    if not isinstance(j, dict):
        raise ValueError("expected object")
    host = j.get("host")
    port = j.get("port")
    if not isinstance(host, str) or not isinstance(port, int):
        raise ValueError("invalid schema")
    return {"host": host, "port": port}
```

## Validation Hooks for Typing Policy

Fail the build if legacy typing forms are found:

```bash
grep -E "from typing import .*(Dict|List|Set|Tuple|Optional|Union)\b" <file>
```

**Allow:** Protocol, TypeVar, ParamSpec, TypedDict, Any (boundary only), TypeAlias

**Report count of `Any` uses and require inline comments at use sites:**

```python
raw: Any  # boundary: json.loads
```

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.