> Run circular import detection against ddtrace and propose architectural fixes for any cycles found. Use this when adding or refactoring modules, or when the detect_circular_imports CI job reports new cycles on a PR.
npx skills add https://github.com/DataDog/dd-trace-py --skill circular-import-analysis
This skill runs the circular import detector locally and proposes sound architectural
fixes for any cycles found. The guiding principle is Separation of Concerns: fixes
must restructure ownership of code, not paper over the problem with deferred imports.
detect_circular_imports CI job reports new cycles on your PR.uv run --script scripts/import-analysis/cycles.py analyze cycles.json
This writes all detected cycles to cycles.json and prints a count to stdout. Requires
uv on PATH (brew install uv or pip install uv).
To read the results:
cat cycles.json # raw JSON array of cycles
Each entry is an array of module names forming the cycle, e.g.:
["ddtrace", "ddtrace.internal.datastreams", "ddtrace"]
Clean up afterwards:
rm cycles.json
> Never use deferred imports (import x inside a function body) as a fix. They hide
> the structural problem, complicate testing and static analysis, and impose a runtime
> cost on every call.
Before proposing a fix, trace why each module needs the other:
# Who imports whom?
grep -rn "^import ddtrace\|^from ddtrace" ddtrace/internal/datastreams/ --include="*.py"
grep -rn "^import ddtrace.internal.datastreams\|^from ddtrace.internal.datastreams" ddtrace/ --include="*.py"
Identify the exact names (classes, functions, constants) that cross the boundary in each
direction. Often only a small fraction of each module is actually involved.
When to use: Two modules share a type (e.g. a dataclass, a Protocol, a constant) that
both need to import, but neither should own.
Before:
ddtrace.foo → ddtrace.bar → ddtrace.foo (cycle)
After:
ddtrace.foo → ddtrace._types (no cycle)
ddtrace.bar → ddtrace._types
Create a thin _types.py (or interfaces.py) module that contains only the shared
contract. Both sides import from it; neither imports from the other. In dd-trace-py,
ddtrace/_trace/types.py and ddtrace/internal/schema.py are examples of this pattern.
When to use: Module A calls into module B, but B also needs to notify A of events.
Before:
ddtrace.core → ddtrace.contrib.foo → ddtrace.core
After:
ddtrace.core → ddtrace._interfaces.IFooHook (abstract)
ddtrace.contrib.foo → ddtrace._interfaces.IFooHook (implements)
Define a Protocol or abstract base in a third module. ddtrace.core depends on the
protocol, not the implementation. The contrib module registers itself at startup
(see the existing ddtrace/internal/hooks.py registration pattern).
When to use: A lower-level module (e.g. ddtrace.internal.X) imports a higher-level
module only to register or configure itself at import time.
Before:
ddtrace → ddtrace.internal.X → ddtrace (X registers itself during import)
After: Remove the registration from ddtrace.internal.X's module scope. Instead,
have ddtrace/__init__.py (the higher-level module) call X.register() explicitly after
importing X. The lower-level module exposes a registration API but does not call it
itself.
This is the standard dd-trace-py pattern: integrations do not self-activate; ddtrace
drives the lifecycle.
When to use: A large module contains both high-level logic (which imports from
elsewhere) and low-level primitives (which are imported by elsewhere). The primitives do
not actually need the high-level logic.
Before:
ddtrace.trace → ddtrace._trace.tracer → ddtrace.trace
After:
ddtrace.trace._api (pure public types / constants — no upward imports)
ddtrace._trace.tracer → ddtrace.trace._api
ddtrace.trace → ddtrace.trace._api
→ ddtrace._trace.tracer
Check whether the parts of the module that are imported by the lower-level module can be
split into a _api.py, _types.py, or _base.py sub-module with no reverse
dependencies.
When to use: The cycle exists because a piece of logic ended up in the wrong module.
This is the simplest and often best fix.
Ask: "Does this function/class conceptually belong in module A or module B?" If it
belongs in A, move it there so that B (which uses it) imports from A — not the other way
round. The cycle disappears because there is now a clear dependency direction.
ddtrace -> ddtrace.internal.datastreams -> ddtrace
ddtrace.trace -> ddtrace._trace.tracer -> ddtrace.internal.datastreams -> ddtrace.trace
ddtrace -> ddtrace.trace -> ddtrace._trace.tracer -> ddtrace.internal.datastreams -> ddtrace
All three cycles pass through ddtrace.internal.datastreams importing something from the
top-level ddtrace or ddtrace.trace packages. The correct investigation path:
ddtrace.internal.datastreams imports from ddtrace / ddtrace.trace: grep -rn "^from ddtrace\b\|^import ddtrace\b" ddtrace/internal/datastreams/ --include="*.py"
ddtrace/internal/ (→ Pattern 5),or whether they represent a shared contract (→ Pattern 1 or 2).
if TYPE_CHECKING guards or function-levelimports as a substitute.
_types.pyif the type already conceptually belongs somewhere.
uv run --script scripts/import-analysis/cycles.py analyze cycles.json after the change and confirming the cycle is gone.Use when receiving code review feedback, before implementing suggestions, especially if feedback seems unclear or technically questionable - requires technical rigor and verification, not performative agreement or blind implementation
Use when completing tasks, implementing major features, or before merging to verify work meets requirements
Execute git commit with conventional commit message analysis, intelligent staging, and message generation. Use when user asks to commit changes, create a git commit, or mentions "/commit". Supports: (1) Auto-detecting type and scope from changes, (2) Generating conventional commit messages from diff, (3) Interactive commit with optional type/scope/description overrides, (4) Intelligent file staging for logical grouping
Comprehensive GitHub code review with AI-powered swarm coordination
Behavioral guidelines to reduce common LLM coding mistakes. Use when writing, reviewing, or refactoring code to avoid overcomplication, make surgical changes, surface assumptions, and define verifiable success criteria.
Use this skill to review code. It supports both local changes (staged or working tree) and remote Pull Requests (by ID or URL). It focuses on correctness, maintainability, and adherence to project standards.
Refactor bloated AGENTS.md, CLAUDE.md, or similar agent instruction files to follow progressive disclosure principles. Splits monolithic files into organized, linked documentation.
Create high-quality git commits: review/stage intended changes, split into logical commits, and write clear commit messages (including Conventional Commits). Use when the user asks to commit, craft a commit message, stage changes, or split work into multiple commits.
Take datadog/circular-import-analysis from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference pip, brew.
Without those the skill loads but fails at the first command.