Install
$ agentstack add skill-mdproctor-cc-praxis-python-dev Open-source listing, not yet scanned by AgentStack. Follow the source repository for install instructions.
Security review
⚠ Flagged1 finding(s); flagged for manual review. · v0.1.0 How review works →
- • Prompt-injection patterns
- • Secret / credential exfiltration
- • Dangerous shell & filesystem operations
- • Untrusted network calls
- • Known-malicious package signatures
- high Dangerous shell/eval execution.
What it can access
- ● Network access Used
- ● Filesystem access Used
- ✓ Shell / process execution No
- ✓ Environment & secrets No
- ● Dynamic code execution Used
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Python Development
Quick Reference
| Category | Rule | How to Apply | |----------|------|--------------| | Safety | Never use mutable default arguments | Use None as default; initialise inside the function body | | | Always use context managers | with for files, DB connections, locks — no manual close() | | | No bare except: | Catch specific exceptions; bare except swallows KeyboardInterrupt | | | Never eval()/exec() on input | Code injection; use ast.literal_eval or structured parsers | | | pathlib.Path over os.path | Cross-platform, composable, readable | | Type Safety | Type-hint all function signatures | Enables mypy, IDE completion, and self-documenting APIs | | | Optional[X] / X \| None for nullable | Caller must handle None; don't let it drift silently | | | Avoid Any | Use Union, TypeVar, or Protocol to preserve type information | | | mypy --strict in CI | Strict mode catches implicit Any, missing return types, untyped imports | | | @dataclass / TypedDict for structured data | Named fields, type-checked, no raw dict guessing | | Async Patterns | Always await coroutines | Unawaited coroutines are silent no-ops — no error, no execution | | | asyncio.gather() for parallel work | Sequential await in a loop forfeits all concurrency benefits | | | Never block the event loop | Use run_in_executor for CPU or blocking I/O | | | async with for async context managers | Ensures __aexit__ runs even on exceptions | | | asyncio.TaskGroup (3.11+) | Structured concurrency; cancels siblings on first failure | | Testing | pytest fixtures over unittest classes | Composable, reusable, cleaner setup/teardown | | | @pytest.mark.parametrize for cases | Table-driven tests — less boilerplate, clearer intent | | | Real implementations over mocks | Mocks drift from production; use them only for external I/O | | | tmp_path fixture for file tests | Isolated temp directory per test; never hardcoded paths | | Code Quality | f-strings over .format() / % | Readable inline, evaluated eagerly, not a template injection risk | | | typing.Final for constants | Communicates intent; mypy rejects reassignment | | | __slots__ in hot classes | Reduces per-instance memory; prevents accidental attribute creation | | | dataclasses.dataclass over manual __init__ | Auto-generates __repr__, __eq__, optional __hash__ |
Rule Priority Decision Flow
flowchart TD
Writing_code((Writing code))
Safety_violation_{Safety violation?}
Apply_Safety_rules[Apply Safety rules]
Type_safety_violation_{Type safety violation?}
Apply_Type_Safety_rules[Apply Type Safety rules]
Async_issue_{Async correctness issue?}
Apply_Async_rules[Apply Async rules]
Apply_Code_Quality_rules[Apply Code Quality rules]
Code_complete((Code complete))
Writing_code --> Safety_violation_
Safety_violation_ -->|"yes (NEVER compromise)"| Apply_Safety_rules
Safety_violation_ -->|no| Type_safety_violation_
Apply_Safety_rules --> Code_complete
Type_safety_violation_ -->|"yes (fix before proceeding)"| Apply_Type_Safety_rules
Type_safety_violation_ -->|no| Async_issue_
Apply_Type_Safety_rules --> Code_complete
Async_issue_ -->|"yes (silent no-op or race)"| Apply_Async_rules
Async_issue_ -->|no| Apply_Code_Quality_rules
Apply_Async_rules --> Code_complete
Apply_Code_Quality_rules --> Code_complete
Priority order: Safety > Type Safety > Async Correctness > Code Quality
Why These Rules Matter
Mutable default arguments causing shared state: A function accumulated user-submitted tags across requests because its default list was shared across all calls. The bug only appeared under load, when multiple users hit the endpoint simultaneously. Fix: def add_tag(tag, tags=None): tags = tags or [].
Missing type hints causing AttributeErrors in production: An internal API changed a field from user_id: str to user: dict. The callers — all untyped — accessed .user_id directly on the response. mypy with type hints would have caught every affected call site at CI time. Fix: typed function signatures + mypy --strict in CI.
Bare except swallowing KeyboardInterrupt: A long-running batch job wrapped its retry loop in except:. A developer tried to stop the job with Ctrl-C; the process ignored the signal and continued running. KeyboardInterrupt and SystemExit are not subclasses of Exception — a bare except: catches everything, including signals Python uses internally. Fix: except Exception: at minimum; except (ValueError, IOError): when the error type is known.
Late-binding closures in loops: A list of callback functions was built in a loop: actions = [lambda: print(i) for i in range(5)]. All five lambdas printed 4 because i was looked up at call time, not capture time. Fix: lambda i=i: print(i) to bind the value, or replace with a named function.
These are real incidents. The rules exist because the pain is real.
Safety
Python's dynamism makes safety issues silent — there's no compiler to catch them. These rules prevent the most common runtime surprises.
Never use mutable default arguments — they are shared across all calls:
# ❌ BAD: The list is created once and shared; tags accumulate across calls
def add_tag(tag: str, tags: list[str] = []) -> list[str]:
tags.append(tag)
return tags
add_tag("a") # ["a"]
add_tag("b") # ["a", "b"] — not ["b"]!
# ✅ GOOD: None sentinel; fresh list on every call
def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
if tags is None:
tags = []
tags.append(tag)
return tags
The same applies to dict, set, and any other mutable type as a default argument.
Always use context managers for resources:
# ❌ BAD: File not closed if an exception occurs between open() and close()
f = open("data.json")
data = json.load(f)
f.close()
# ✅ GOOD: __exit__ always runs, even on exception
with open("data.json") as f:
data = json.load(f)
Context managers apply equally to database connections, network sockets, threading locks, and any resource with an __enter__/__exit__ pair.
Never catch bare except: — catch specific exceptions:
# ❌ BAD: Catches KeyboardInterrupt, SystemExit, GeneratorExit — blocks Ctrl-C
try:
result = risky_operation()
except:
logger.error("Something went wrong")
# ✅ GOOD: Catch only what you can handle
try:
result = risky_operation()
except (ValueError, IOError) as exc:
logger.error("Operation failed: %s", exc)
raise
Minimum acceptable: except Exception:. But name the exceptions you expect.
Never eval() or exec() on untrusted input:
# ❌ BAD: Executes arbitrary code from the request
user_filter = request.args.get("filter")
result = eval(user_filter) # Code injection
# ✅ GOOD: Parse structured input explicitly
allowed_filters = {"active", "inactive", "pending"}
user_filter = request.args.get("filter")
if user_filter not in allowed_filters:
raise ValueError(f"Invalid filter: {user_filter!r}")
For trusted config files with simple literals, use ast.literal_eval — it evaluates only Python literals, not arbitrary expressions.
Use pathlib.Path instead of string concatenation for paths:
# ❌ BAD: String join breaks on Windows; hard to read
config_path = base_dir + "/config/" + env + ".yaml"
# ✅ GOOD: Composable, OS-aware, readable
config_path = Path(base_dir) / "config" / f"{env}.yaml"
Type Safety
Python's type system is optional, which is exactly why you must opt into it deliberately. Untyped code is correct until it isn't — and the failure surfaces at runtime in production.
Always add type hints to function signatures:
# ❌ BAD: Caller has no idea what types are expected or returned
def process_user(user, flags):
...
# ✅ GOOD: Contract is explicit; mypy can verify callers
def process_user(user: User, flags: set[str]) -> ProcessResult:
...
Use Optional[X] or X | None for nullable values (Python 3.10+: prefer X | None):
# ❌ BAD: Caller doesn't know None is possible; AttributeError at runtime
def find_user(user_id: str) -> User:
return db.get(user_id) # Returns None if not found
# ✅ GOOD: None is part of the contract; mypy enforces null checks on callers
def find_user(user_id: str) -> User | None:
return db.get(user_id)
Avoid Any — preserve type information with Union, TypeVar, or Protocol:
# ❌ BAD: Any propagates — typed code calling this function loses its types
def deserialise(data: Any) -> Any:
...
# ✅ GOOD: TypeVar preserves the relationship between input and output
T = TypeVar("T")
def deserialise(data: bytes, model: type[T]) -> T:
...
Run mypy --strict in CI. This enables --disallow-untyped-defs, --warn-return-any, --no-implicit-optional, and related checks. A codebase without strict mypy is typed in name only.
Use @dataclass or TypedDict for structured data instead of plain dicts:
# ❌ BAD: No type checking; typos in key names silently produce None
user = {"user_id": "abc", "emal": "x@y.com"} # Typo — undetected
send_confirmation(user["email"]) # KeyError at runtime
# ✅ GOOD: Named fields, type-checked, IDE-autocompleted
@dataclass
class User:
user_id: str
email: str
user = User(user_id="abc", email="x@y.com")
send_confirmation(user.email) # Typo → AttributeError caught by mypy
Async Patterns
Async bugs are among the hardest to reproduce — they often only surface under load or specific timing windows. Python's asyncio is strict: unawaited coroutines do nothing and emit only a RuntimeWarning that is easy to miss in logs.
Always await coroutines — unawaited coroutines are silent no-ops:
# ❌ BAD: save_order returns a coroutine object — it never runs
async def handle_request(order: Order) -> None:
save_order(order) # No await — silently does nothing
send_confirmation(order) # Also no await
# ✅ GOOD: Explicit await — execution is guaranteed, errors propagate
async def handle_request(order: Order) -> None:
await save_order(order)
await send_confirmation(order)
Use asyncio.gather() for parallel work, not sequential awaits in a loop:
# ❌ BAD: Each fetch waits for the previous to complete — O(N) latency
async def load_all(ids: list[str]) -> list[Record]:
results = []
for record_id in ids:
record = await fetch_record(record_id)
results.append(record)
return results
# ✅ GOOD: All fetches in flight simultaneously — O(1) latency
async def load_all(ids: list[str]) -> list[Record]:
return await asyncio.gather(*[fetch_record(i) for i in ids])
Use asyncio.gather(*coros, return_exceptions=True) when one failure should not abort the others.
Never block the event loop — use run_in_executor for CPU or blocking I/O:
# ❌ BAD: time.sleep blocks the event loop; all other coroutines are frozen
async def poll() -> None:
time.sleep(1) # Blocks entire thread
result = do_cpu_work() # Same problem
# ✅ GOOD: Async sleep yields; executor runs blocking code in a thread pool
async def poll() -> None:
await asyncio.sleep(1)
loop = asyncio.get_running_loop()
result = await loop.run_in_executor(None, do_cpu_work)
Use asyncio.TaskGroup (Python 3.11+) over manual task management:
# ❌ BAD: Manual task tracking; exceptions may go unobserved
tasks = [asyncio.create_task(fetch(i)) for i in ids]
results = await asyncio.gather(*tasks)
# ✅ GOOD: TaskGroup cancels all siblings on first exception; cleaner lifecycle
async with asyncio.TaskGroup() as tg:
tasks = [tg.create_task(fetch(i)) for i in ids]
results = [t.result() for t in tasks]
Testing
pytest with fixtures over unittest classes:
# ❌ BAD: setUp/tearDown are rigid; test methods share namespace
class TestUserService(unittest.TestCase):
def setUp(self):
self.service = UserService(FakeRepo())
# ✅ GOOD: Fixtures are composable and reusable across modules
@pytest.fixture
def user_service() -> UserService:
return UserService(InMemoryUserRepo())
def test_creates_user(user_service: UserService) -> None:
user = user_service.create(name="Alice", email="alice@example.com")
assert user.name == "Alice"
Use @pytest.mark.parametrize for table-driven tests:
# ❌ BAD: One test per case — repetitive, hard to extend
def test_valid_email_foo():
assert is_valid_email("foo@example.com")
def test_valid_email_bar():
assert is_valid_email("bar@example.com")
# ✅ GOOD: All cases in one declaration; failure output shows the failing input
@pytest.mark.parametrize("email,expected", [
("foo@example.com", True),
("not-an-email", False),
("", False),
("a@b.c", True),
])
def test_email_validation(email: str, expected: bool) -> None:
assert is_valid_email(email) == expected
Prefer real implementations over mocks; mock only external I/O:
# ❌ BAD: Mock diverges from the real repository contract over time
mock_repo = MagicMock(spec=UserRepository)
mock_repo.find_by_id.return_value = User(id="1", email="a@b.com")
# ✅ GOOD: In-memory implementation honours the real interface
class InMemoryUserRepo:
def __init__(self) -> None:
self._store: dict[str, User] = {}
def save(self, user: User) -> None:
self._store[user.id] = user
def find_by_id(self, user_id: str) -> User | None:
return self._store.get(user_id)
Use the tmp_path fixture for file tests — never hardcoded paths:
# ❌ BAD: Leaves state on disk; breaks in parallel test runs
def test_writes_config():
write_config("/tmp/config.yaml", {"key": "value"})
assert Path("/tmp/config.yaml").exists()
# ✅ GOOD: Isolated per-test temp directory; cleaned up automatically
def test_writes_config(tmp_path: Path) -> None:
config_file = tmp_path / "config.yaml"
write_config(config_file, {"key": "value"})
assert config_file.exists()
Type-check tests too — don't exempt test files from mypy. Test code calls the same public API as production code. Exempt test files miss type errors on public API changes.
⛔ Bug Fix Workflow — Mandatory
See ~/.hortora/garden/approaches/testing.md — Bug Fix Workflow section for the mandatory 5-step process.
Code Quality
Use typing.Final for constants — communicates intent, mypy rejects reassignment:
# ❌ BAD: Any code can reassign MAX_RETRIES inadvertently
MAX_RETRIES = 3
# ✅ GOOD: Final signals to humans and tools that this value is invariant
from typing import Final
MAX_RETRIES: Final = 3
Use __slots__ in performance-critical classes:
# ❌ BAD: Each instance carries a full __dict__ — O(N) memory overhead
class Point:
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
# ✅ GOOD: Fixed attribute layout — less memory, faster attribute access
class Point:
__slots__ = ("x", "y")
def __init__(self, x: float, y: float) -> None:
self.x = x
self.y = y
Prefer dataclasses.dataclass over manual __init__:
# ❌ BAD: Manual __init__, __repr__, __eq__ — repetitive boilerplate
class Config:
def __init__(self, host: str, port: int, debug: bool = False):
self.host = host
self.port = port
self.debug = debug
# ✅ GOOD: Generated __init__, __repr__, __eq__; frozen=True makes it immutable
@dataclass(frozen=True)
class Config:
host: str
port: int
debug: bool = False
f-strings over .format() or %:
# ❌ BAD: Format strings break on rename; % format is error-prone with tuples
msg = "User %s logged in from %s" % (user.name, ip_address)
msg = "User {} logged in from {}".format(
…
## Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- **Author:** [mdproctor](https://github.com/mdproctor)
- **Source:** [mdproctor/cc-praxis](https://github.com/mdproctor/cc-praxis)
- **License:** Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.