/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
$ npx -y skills add mindfold-ai/trellis --skill python-design --agent claude-codeHow 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.mdname: 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: strFrozen 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():
passThe 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
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: strFrozen 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():
passThe 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
Repo: mindfold-ai/trellis
Other skills on trellis.
- /contribute
Guide for contributing to Trellis documentation and marketplace. Covers adding spec templates, marketplace skills, documentation pages, and submitting PRs across both the Trellis main repo and docs repo. Use when someone wants to add a new spec template, add a new skill to the
Open skill - /first-principles-thinking
Systematic first principles thinking for any problem domain. Use when the user says "analyze from first principles", "第一性原理", "从根本分析", "从零开始思考", "think from scratch", "question this design", "is this the right approach", "challenge assumptions", "挑战假设", "为什么要这样做", "有没有更好的方案",
Open skill - /gitnexus-cli
Use when the user needs to run GitNexus CLI commands like analyze/index a repo, check status, clean the index, generate a wiki, or list indexed repos. Examples: \"Index this repo\", \"Reanalyze the codebase\", \"Generate a wiki\"
Open skill - /gitnexus-debugging
Use when the user is debugging a bug, tracing an error, or asking why something fails. Examples: \"Why is X failing?\", \"Where does this error come from?\", \"Trace this bug\"
Open skill - /gitnexus-exploring
Use when the user asks how code works, wants to understand architecture, trace execution flows, or explore unfamiliar parts of the codebase. Examples: \"How does X work?\", \"What calls this function?\", \"Show me the auth flow\"
Open skill - /gitnexus-guide
Use when the user asks about GitNexus itself — available tools, how to query the knowledge graph, MCP resources, graph schema, or workflow reference. Examples: \"What GitNexus tools are available?\", \"How do I use GitNexus?\"
Open skill

