Skip to content
Development
Skill

/python-design

Python design patterns for CLI scripts and utilities — type-first development, deep modules, complexity management, and red flags. Use when reading, writing, reviewing, or refactoring Python files, especially in .trellis/scripts/ or any CLI/scripting context. Also activate when

From plugin
trellis
14k18 skills5 agents6 commands
Install
$ npx -y skills add mindfold-ai/trellis --skill python-design --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/python-design

Context preview

The summary Claude sees to decide when to auto-load this skill.

Python design patterns for CLI scripts and utilities — type-first development, deep modules, complexity management, and red flags. Use when reading, writing, reviewing, or refactoring Python files, especially in .trellis/scripts/ or any CLI/scripting context. Also activate when

SKILL.md

python-design.SKILL.md
name: python-design
description: "Python design patterns for CLI scripts and utilities — type-first development, deep modules, complexity management, and red flags. Use when reading, writing, reviewing, or refactoring Python files, especially in .trellis/scripts/ or any CLI/scripting context. Also activate when planning module structure, deciding where to put new code, or doing code review."

Python Design for CLI Scripts

Design patterns and principles for writing maintainable Python CLI tools and utilities. Based on *A Philosophy of Software Design* (Ousterhout), adapted for scripting contexts.

When to Activate

  • Writing or modifying Python files
  • Planning module decomposition
  • Code review of Python changes
  • Refactoring scripts that feel "messy"
  • Adding a new subcommand or utility function

Core Thesis

**The central challenge is managing complexity, not adding features.**

Complexity is anything that makes code hard to understand or modify. It has three symptoms:

1. **Change Amplification** — A small change requires edits in many places 2. **Cognitive Load** — You must hold too much context to make a safe change 3. **Unknown Unknowns** — You don't know what you don't know (the most dangerous)

Complexity is incremental. It accumulates through hundreds of small decisions, not one catastrophic mistake. Therefore: **sweat the small stuff**.

---

Principle 1: Deep Modules

A module's value is the ratio of functionality hidden vs. interface exposed.

Deep module (good):          Shallow module (bad):
┌──────────┐                 ┌──────────────────────────┐
│ simple   │                 │ complex interface        │
│ interface│                 │ many params, many methods │
├──────────┤                 ├──────────────────────────┤
│          │                 │                          │
│  rich    │                 │  thin implementation     │
│  impl    │                 │                          │
│          │                 └──────────────────────────┘
│          │
└──────────┘

**Practical test**: If a caller must understand how the module works internally to use it correctly, the module is too shallow.

Example: Task Data Access

# Shallow — caller must know JSON structure, file paths, error handling
def _read_json_file(path: Path) -> dict:
    with open(path, encoding="utf-8") as f:
        return json.load(f)

# Every caller does this independently:
task_path = tasks_dir / name / "task.json"
data = _read_json_file(task_path)
title = data.get("title") or data.get("name", "")
status = data.get("status", "planning")
assignee = data.get("assignee", "")
# Deep — caller gets what they need, module hides JSON/path/parsing
@dataclass(frozen=True)
class TaskInfo:
    name: str
    title: str
    status: str
    assignee: str
    priority: str
    directory: Path

def load_task(tasks_dir: Path, name: str) -> TaskInfo | None:
    """Load task by directory name. Returns None if not found."""
    ...

def list_active_tasks(tasks_dir: Path) -> list[TaskInfo]:
    """List all non-archived tasks, sorted by priority."""
    ...

The deep version absorbs complexity: JSON parsing, field defaults, directory scanning, archive filtering. Callers just work with typed data.

---

Principle 2: Type-First Development

Types define contracts before implementation. This workflow catches design problems early:

1. **Define data shapes** — dataclass or TypedDict first 2. **Define function signatures** — parameter and return types 3. **Implement to satisfy types** — let the type checker guide completeness 4. **Validate at boundaries** — runtime checks only where data enters the system

Frozen Dataclasses for Internal Data

from dataclasses import dataclass
from typing import Literal

@dataclass(frozen=True)
class AgentRecord:
    agent_id: str
    task_name: str
    worktree_path: Path
    platform: Literal["claude", "codex", "cursor"]
    status: Literal["running", "done", "failed"]
    branch: str

Frozen dataclasses are immutable — no accidental mutation, safe to pass around.

TypedDict for External JSON Shapes

When the data comes from a file (task.json, config.yaml, registry.json), use TypedDict to document the expected shape:

from typing import TypedDict, Required, NotRequired

class TaskData(TypedDict):
    title: Required[str]
    status: Required[str]
    assignee: NotRequired[str]
    priority: NotRequired[str]
    parent: NotRequired[str]
    children: NotRequired[list[str]]

This eliminates scattered `.get("field", default)` calls — the shape is documented once.

NewType for Domain Primitives

When two strings mean different things, make the type system enforce it:

from typing import NewType

TaskName = NewType("TaskName", str)    # directory name like "03-10-v040"
BranchName = NewType("BranchName", str)  # git branch like "feat/v0.4.0"

def create_branch(task: TaskName) -> BranchName:
    return BranchName(f"task/{task}")

Discriminated Unions for State

When an entity can be in distinct states with different data:

@dataclass(frozen=True)
class Pending:
    status: Literal["pending"] = "pending"

@dataclass(frozen=True)
class Running:
    status: Literal["running"] = "running"
    pid: int
    worktree: Path

@dataclass(frozen=True)
class Completed:
    status: Literal["completed"] = "completed"
    branch: str
    commit: str

AgentState = Pending | Running | Completed

def handle(state: AgentState) -> None:
    match state:
        case Running(pid=pid, worktree=wt):
            check_process(pid)
        case Completed(branch=br):
            create_pr(br)
        case Pending():
            pass

The type checker ensures every state is handled. No more `if data.get("status") == "running"` with forgotten branches.

---

Principle 3: Information Hiding

Each module should encapsulate design decisions. When the same knowledge appears in multiple module

Read more
Ships withtrellis

The best agent harness.

Get the whole plugin

Other skills on trellis.