| name | anti-slop-python |
|---|---|
| description | Simplify Python code by removing defensive over-engineering, unnecessary abstractions, generic dictionaries, excessive runtime checks, wrapper helpers, and AI-generated architectural noise. |
Apply these rules whenever modifying Python code.
The goal is simple, explicit, typed, idiomatic Python that is easy to trace and does not defend against impossible internal states.
The default philosophy is:
Validate untrusted data at the boundary. Use precise types and straightforward Python everywhere else.
Prefer boring code over clever abstractions.
Search aggressively for:
Any
dict[str, Any]
Mapping[str, Any]
list[Any]
object
inside normal application logic.
If the shape is known, define it.
Bad:
def get_domain(data: dict[str, Any]) -> str:
value = data.get("domain")
if not isinstance(value, str):
return ""
return value
Prefer:
@dataclass
class DomainEvent:
domain: str
Then:
event.domain
Or use the project's existing model system:
class DomainEvent(BaseModel):
domain: str
Do not carry untyped dictionaries deep into the application.
Be suspicious of functions such as:
as_string
to_string
safe_string
string_value
to_int
as_int
safe_int
to_bool
to_dict
as_dict
ensure_dict
normalize_value
Bad:
def to_string(value: Any) -> str:
if value is None:
return ""
if isinstance(value, str):
return value
if isinstance(value, int):
return str(value)
if isinstance(value, ObjectId):
return str(value)
return repr(value)
Ask:
What is this value actually supposed to be?
If it is a string:
def normalize_domain(domain: str) -> str:
return domain.strip().lower()
Do not make every function capable of accepting arbitrary Python values.
Bad:
def normalize_domain(value: Any) -> str:
if isinstance(value, str):
return value.strip().lower()
if isinstance(value, int):
return str(value).strip().lower()
return ""
A domain name should not randomly be an integer.
Prefer:
def normalize_domain(domain: str) -> str:
return domain.strip().lower()
Types should describe valid application states.
Do not broaden inputs solely to make code more "defensive."
Bad:
def as_project(value: Any) -> Project:
if not isinstance(value, dict):
return {}
return cast(Project, value)
This does not validate Project.
Either validate at the boundary using the project's real validation mechanism, or trust the value at a known integration point.
Do not write several runtime checks and still finish with cast().
Do not treat every cast() as inherently bad.
Bad cleanup:
if (
isinstance(error, Exception)
and hasattr(error, "code")
and isinstance(error.code, int)
and error.code == DUPLICATE_KEY_ERROR_CODE
):
...
when this is enough at a known driver boundary:
return cast(Any, error).code == DUPLICATE_KEY_ERROR_CODE
Better still, use the driver's actual exception type if available:
except DuplicateKeyError:
...
Do not turn one simple, understood assumption into five branches merely to avoid a cast.
Do not manually inspect generic exceptions when the library exposes a concrete exception.
Bad:
except Exception as exc:
if getattr(exc, "code", None) == 11000:
...
Prefer:
except DuplicateKeyError:
...
Use exception APIs provided by the dependency.
Do not reinvent error classification.
Be suspicious of:
try:
...
except Exception:
return None
or:
try:
...
except Exception:
pass
or:
try:
...
except Exception as exc:
logger.error(exc)
return {}
Catch exceptions you can actually handle.
Do not silently convert unexpected programming errors into empty values.
Avoid:
try:
...
except:
pass
except at extremely deliberate process-level boundaries.
Bare except also catches system-exiting exceptions.
Use specific exception types.
Bad:
try:
return await service.run()
except Exception:
raise
Delete the try/except.
Likewise:
try:
...
except Exception as exc:
raise exc
is usually worse than letting the exception propagate naturally.
Bad:
try:
await operation()
except Exception:
return None
If failure is acceptable, make that business rule explicit.
If it is not acceptable, let the error propagate.
Silent failure is not robustness.
Bad:
try:
return repository.get_project(project_id)
except Exception:
logger.exception("Failed to get project")
raise
if an outer handler also logs the same error.
Prefer returning/raising errors through internal layers and logging once at the responsible boundary.
Avoid duplicate stack traces.
Search for suspicious patterns like:
return ""
return {}
return []
return None
value or ""
value or {}
value or []
data.get("field", "")
data.get("field", {})
when the fallback hides an invalid state.
Bad:
project_id = data.get("project_id", "")
if project_id is required.
Prefer validation at the boundary and then:
project_id = data.project_id
Do not hide missing required values.
Bad:
name = project.get("name")
region = project.get("region")
config = project.get("config", {})
when the schema guarantees those keys.
If it is a typed dictionary:
class ProjectData(TypedDict):
name: str
region: str
config: ProjectConfig
prefer:
project["name"]
project["region"]
project["config"]
Or better, use a model/dataclass where appropriate.
Use .get() when absence is actually valid.
Bad:
domain = (
data.get("project", {})
.get("config", {})
.get("domain", {})
.get("name")
)
This usually hides malformed input.
Prefer modeling the structure properly.
data.project.config.domain.name
or validated dictionary access.
Do not turn invalid nested structures into None silently.
Be suspicious of:
dict[str, Any]
Mapping[str, Any]
MutableMapping[str, Any]
moving between:
handler
β service
β manager
β repository
β processor
If the schema is known, define a model.
Possible tools include:
- dataclasses
- TypedDict
- Pydantic models
- attrs
- domain classes
Use whichever the project already uses.
Do not introduce another modeling framework unnecessarily.
Good:
class QueuePayload(TypedDict):
event: str
data: dict[str, Any]
can be appropriate at a JSON-shaped boundary.
But do not turn the entire domain model into nested TypedDicts if actual objects would make business logic clearer.
Use the simplest representation that fits the project.
Dataclasses are useful when a plain object represents structured internal data.
Good:
@dataclass(frozen=True)
class DomainMapEvent:
project_id: str
domain: str
Avoid adding:
__post_init__
classmethod factories
builder methods
conversion methods
validation methods
unless they enforce meaningful invariants.
A dataclass should not become a mini-framework.
Pydantic is excellent at boundaries.
It does not need to become the base representation for every internal object.
Use it where validation/parsing is valuable:
- HTTP requests
- environment/config
- external payloads
- queue events
- API responses
Inside trusted application code, plain dataclasses or typed objects may be simpler.
Follow the codebase's existing style.
Bad:
request = ProjectRequest.model_validate(payload)
...
service.create_project(request)
...
if not request.project_id:
raise ValueError(...)
...
if not isinstance(request.project_id, str):
...
If the boundary model already validated it, trust it.
Do not revalidate the same object at every layer.
Bad:
if not isinstance(project.id, str):
return None
when:
@dataclass
class Project:
id: str
already guarantees it.
Trust internal types.
If the type is inaccurate, fix the type.
Be suspicious of:
if hasattr(value, "id"):
...
or:
domain = getattr(document, "domain", None)
when the object has a known type.
Prefer:
document.domain
Use getattr for truly dynamic APIs, not as general defensive programming.
Bad:
project_id = getattr(project, "id", "")
if project.id is required.
Prefer:
project.id
If the object may genuinely be absent:
if project is None:
raise ProjectNotFound(...)
Then continue normally.
Be suspicious of:
getattr
setattr
hasattr
vars
__dict__
inspect
dir
inside ordinary business logic.
Python is dynamic, but that does not mean application code should discover its own shape at runtime.
Use explicit attributes and types.
If a feature relies heavily on:
inspect.signature
inspect.getmembers
inspect.isclass
ask whether the code is building infrastructure/framework behavior or simply avoiding explicit APIs.
Reflection is appropriate in frameworks/tooling.
It is suspicious in normal domain logic.
Bad:
def serialize_value(value: Any) -> str:
if isinstance(value, str):
return value
if isinstance(value, int):
return str(value)
if isinstance(value, ObjectId):
return str(value)
if isinstance(value, datetime):
return value.isoformat()
return repr(value)
unless arbitrary-value serialization is genuinely the feature.
If you know the field type, serialize that type directly.
Be suspicious of:
domain_id_from
project_id_from
extract_id
extract_name
resolve_field
safe_field
object_id_from
string_from
Bad:
def domain_id_from(document: dict[str, Any] | None) -> str | None:
if document is None:
return None
domain = document.get("domain")
if not isinstance(domain, ObjectId):
return None
return str(domain)
If the document contract is known:
@dataclass
class DNSDocument:
domain: ObjectId
then:
str(document.domain)
Fix typing rather than introducing another helper.
Be suspicious of functions that:
- have one caller
- are one to three lines
- only access a property
- only call
.strip() - only call
.lower() - only perform
isinstance - only return a fallback
- only forward parameters
- only rename another function
Bad:
def get_project_id(project: Project) -> str:
return project.id
Prefer:
project.id
Helpers should represent real concepts.
Do not split simple logic into:
parser.py
normalizer.py
validator.py
converter.py
mapper.py
resolver.py
helper.py
processor.py
manager.py
for a tiny feature.
Keep related code together when it improves readability.
Separation of concerns does not mean separation of every statement.
Be suspicious of:
Controller
β Service
β Manager
β Processor
β Handler
β Repository
β DAO
when most layers simply forward parameters.
Python does not need enterprise ceremony.
Collapse layers that add no logic.
Bad:
class ProjectManager:
def get_project(self, project_id: str) -> Project:
return self.project_service.get_project(project_id)
and then:
class ProjectService:
def get_project(self, project_id: str) -> Project:
return self.repository.get_project(project_id)
If a layer adds no policy, transformation, caching, orchestration, or meaningful abstraction, remove it.
Be suspicious of:
class BaseService:
...
class BaseRepository:
...
class AbstractManager:
...
when subclasses share little meaningful behavior.
Do not create inheritance hierarchies just to centralize two utility methods.
Prefer composition or direct code.
Bad:
class MongoProjectRepository(BaseRepository, LoggingMixin, RetryMixin):
...
when dependencies/functions can be explicit.
Multiple inheritance and mixin stacks make behavior hard to trace.
Use them only when they genuinely simplify the architecture.
Be suspicious of:
LoggingMixin
ValidationMixin
SerializationMixin
RetryMixin
TimestampMixin
ErrorHandlingMixin
for ordinary application classes.
Mixins often hide dependencies and control flow.
Prefer explicit calls or composition.
Do not create:
class ProjectRepository(Protocol):
...
class AbstractProjectRepository(ABC):
...
when there is one concrete implementation and no real abstraction need.
Protocols/interfaces are useful for narrow consumer contracts and interchangeable implementations.
Do not add them because "good architecture requires interfaces."
Bad:
class ProjectGetter(Protocol):
def get_project(...):
...
created solely because one test needs a mock.
Python's testing ecosystem already supports dependency substitution easily.
Use protocols when they express a meaningful contract.
Bad:
class RepositoryFactory:
def create(self, type_: str) -> Repository:
...
when the application always uses one repository.
Prefer direct construction.
Factories should solve actual runtime selection or complex setup.
Bad:
deployment = (
DeploymentBuilder()
.with_project_id(project_id)
.with_region(region)
.with_port(port)
.build()
)
Prefer:
deployment = Deployment(
project_id=project_id,
region=region,
port=port,
)
Python already has excellent object construction syntax.
Do not emulate Java builders unnecessarily.
Be suspicious of:
Project.from_dict(...)
Project.from_payload(...)
Project.from_model(...)
Project.from_entity(...)
Project.from_record(...)
when the transformations are trivial or duplicate one another.
Use alternative constructors only when they express genuinely different construction logic.
Audit:
to_dict
from_dict
to_model
from_model
to_dto
from_dto
to_entity
from_entity
to_schema
from_schema
If two representations are nearly identical, question why both exist.
Do not maintain fleets of copy-field transformations without a real boundary distinction.
Be suspicious of:
Project
ProjectDTO
ProjectData
ProjectPayload
ProjectRequest
ProjectResponse
ProjectModel
ProjectEntity
ProjectRecord
with almost the same fields.
Separate models where API/persistence/domain contracts genuinely differ.
Do not duplicate types just because each layer supposedly needs its own model.
Python dependency injection can simply be:
service = ProjectService(repository, logger)
Do not introduce:
Container
Registry
Provider
Resolver
ServiceLocator
DependencyGraph
without a real need.
Explicit construction is easier to trace.
Bad:
repo = services.get("project_repository")
Prefer explicit dependencies.
Likewise, avoid mutable module-level globals for application services/config where explicit wiring is practical.
Bad:
class DatabaseSingleton:
_instance = None
@classmethod
def get_instance(cls):
...
unless the lifecycle genuinely requires it.
Usually the application startup layer can create one instance and pass it around.
Do not add wrapper utilities around normal context manager behavior without value.
Bad:
def safe_transaction(db):
return TransactionContext(db)
when:
with db.transaction():
...
already expresses the operation clearly.
Do use them for real resources:
with open(path) as file:
async with client.stream(...) as response:
with transaction:
Do not replace appropriate resource management merely to reduce lines.
Be suspicious of:
@retry(...)
def everything():
Retries are not generic safety.
Only retry operations that are:
- transient
- idempotent or safe to repeat
- appropriate for retry semantics
Do not add retries around arbitrary business logic.
Be suspicious when functions accumulate:
@retry
@log_execution
@validate
@measure
@catch_errors
@authorize
@normalize
Decorators hide control flow.
Use them for genuinely cross-cutting concerns with stable semantics.
Do not turn basic logic into a decoration stack.
Bad:
@ensure_not_none
def process_project(...):
when:
if project is None:
raise ProjectNotFound(...)
is clearer.
Explicit logic is often better than decorator magic.
Bad:
if project is not None:
if project.enabled:
if project.status == "active":
Prefer:
if project is None:
raise ProjectNotFound(project_id)
if not project.enabled:
return
if project.status != "active":
return
Keep the happy path obvious.
Bad:
if enabled is True:
when:
if enabled:
is equivalent.
Bad:
if enabled == False:
Prefer:
if not enabled:
Use explicit is True only when tri-state behavior genuinely matters.
Bad:
value = "a" if active else "b" if enabled else "c"
Prefer normal control flow.
Do not compress logic at the expense of readability.
Bad:
raw_domain = event.domain
trimmed_domain = raw_domain.strip()
normalized_domain = trimmed_domain.lower()
domain = normalized_domain
Prefer:
domain = event.domain.strip().lower()
Use intermediate names only when they clarify meaningful concepts.
Be suspicious of:
return list(items)
or:
copy = items[:]
when ownership/mutation does not require a copy.
Do not allocate defensively without a real reason.
Likewise:
return dict(config)
should have a concrete ownership reason.
Do not copy mutable structures mechanically.
Search for:
copy.deepcopy(...)
Deep copying can be expensive and usually signals unclear ownership.
Use it only when nested mutation isolation is actually required.
Do not add:
@lru_cache
@cache
to functions without understanding:
- lifecycle
- cardinality
- invalidation
- memory growth
- stale data behavior
Caching is architecture, not a free optimization.
Do not make functions async just because surrounding code is async.
Bad:
async def normalize_domain(domain: str) -> str:
return domain.strip().lower()
Prefer synchronous functions for synchronous work.
Be suspicious of:
asyncio.create_task(...)
added simply to "not block."
Every background task raises questions about:
- ownership
- cancellation
- exception handling
- shutdown
- ordering
- lifetime
Use task creation deliberately.
Bad:
asyncio.create_task(send_event())
with no task tracking or error handling.
If the result matters, await it.
If fire-and-forget is intentional, ensure the application owns the task lifecycle.
Do not turn two trivial sequential calls into concurrency automatically.
Use concurrent execution when operations are independent and actually benefit from overlapping I/O.
Do not make control flow harder for theoretical speedups.
Avoid:
asyncio.Lock()
threading.Lock()
without real shared mutable state and concurrency.
Locks create lifecycle and deadlock complexity.
Protect actual races, not hypothetical ones.
Do not wrap already-async libraries in:
asyncio.to_thread(...)
run_in_executor(...)
without need.
Use thread off for genuinely blocking operations.
Do not introduce workers/process pools for small CPU work without evidence.
Measure first.
Simple code first.
Be suspicious of:
@dataclass
class Result(Generic[T]):
value: T | None
error: Exception | None
success: bool
Python already has exceptions.
Do not emulate Rust/Go-style result handling unless the codebase deliberately uses that model.
Question:
str | None
Project | None
Config | None
when the value is actually required after construction.
Do not model required internal state as nullable purely because data initially enters incompletely.
Parse/build a valid object first.
Bad:
if project is None:
return None
if project.config is None:
return None
if project.config.domain is None:
return None
when the object contract says these are required.
Fix the model.
Use None only for real optionality.
Bad:
port = value or 8080
when 0 might have meaning.
Bad:
enabled = value or True
Use explicit None handling when appropriate:
port = 8080 if value is None else value
Do not conflate falsey with missing.
Be suspicious of:
str(value)
int(value)
bool(value)
float(value)
used as "validation."
For example:
bool("false")
is True.
Do not silently coerce malformed external data.
Parse it according to its real contract.
Good:
class DeploymentStatus(StrEnum):
PENDING = "pending"
RUNNING = "running"
FAILED = "failed"
when the valid states are closed and domain-significant.
Do not create an enum for every arbitrary string.
Be suspicious of:
@dataclass
class ProjectID:
value: str
when a string is sufficient.
A custom type can be useful when it provides real validation or domain behavior.
Do not wrap every primitive.
Bad:
class ProjectList:
def __init__(self, projects: list[Project]):
self._projects = projects
with methods that merely proxy list behavior.
Use built-in collections unless a domain abstraction provides real value.
Before keeping custom helpers, check whether Python already has the operation.
Prefer:
str.strip
str.lower
pathlib.Path
collections.defaultdict
itertools
functools
dataclasses
enum
contextlib
urllib.parse
where appropriate.
Do not maintain custom versions of standard behavior.
Audit modules/packages called:
utils.py
helpers.py
common.py
shared.py
misc.py
base.py
core.py
These often collect unrelated functions.
Delete trivial helpers.
Move domain-specific logic to the domain that owns it.
Do not create another generic utility module during cleanup.
Do not create a new file for every class/function.
Bad:
domain_parser.py
domain_normalizer.py
domain_validator.py
domain_converter.py
for four tiny functions.
Group cohesive functionality.
File count is not architecture quality.
A module can contain several closely related functions.
Do not interpret "single responsibility" as "one function per file."
Optimize for discoverability.
Be suspicious of:
HANDLERS = {}
register_handler(...)
register_service(...)
PLUGIN_REGISTRY = {}
when a normal match/dictionary literal/import is sufficient.
Use dynamic registration only when extensibility is genuinely required.
Bad:
handler = registry.resolve(event.type)
return handler.process(event)
when:
match event.type:
case "insert":
return handle_insert(event)
case "delete":
return handle_delete(event)
is clearer.
Do not turn static cases into plugin architecture.
Three branches do not automatically need:
BaseStrategy
InsertStrategy
DeleteStrategy
ReplaceStrategy
StrategyFactory
Use ordinary Python control flow when easier to understand.
Bad:
processor(value, lambda x: normalize(x))
when:
processor(value, normalize)
is enough.
Or simply:
normalize(value)
if the abstraction itself is unnecessary.
Do not turn complex business logic into dense comprehensions.
Bad:
result = {
x.id: transform(x)
for x in items
if x.enabled and x.config and x.config.valid
}
when a loop would make failure cases and rules clearer.
Comprehensions are for simple transformations.
Bad:
return next((x for x in values if x.id == id_), None)
is fine when obvious.
But do not compress multi-step business logic into nested expressions simply to save lines.
Readability first.
Be suspicious of:
map(...)
filter(...)
reduce(...)
partial(...)
when a simple loop is clearer.
Python often reads better with comprehensions or direct loops.
Do not optimize for abstract functional style.
Bad:
list(map(lambda x: x.id, projects))
Prefer:
[project.id for project in projects]
Use idiomatic Python.
Bad:
def is_empty(value: str) -> bool:
return len(value) == 0
Prefer:
if not value:
when semantics match.
Do not wrap obvious built-ins for no reason.
Bad:
re.sub(r"^\s+|\s+$", "", domain)
Prefer:
domain.strip()
Use regex for regex-shaped problems.
Do not use dynamic code execution to solve configuration, expression, or dispatch problems unless the feature explicitly requires it and security implications are understood.
Prefer explicit parsing.
Do not patch classes/functions at runtime to avoid proper dependency design.
Monkey patching may be appropriate in tests or specialized libraries.
It should not be normal application architecture.
Metaclasses are rarely required in ordinary application code.
Be highly suspicious of introducing:
class FooMeta(type):
...
for registration, validation, or convenience.
Prefer normal classes/decorators/functions.
Likewise, do not introduce custom descriptors for basic validation or computed fields when properties/dataclasses/models solve the problem more clearly.
Bad:
class Project:
@property
def id(self) -> str:
return self._id
when there is no invariant or encapsulation need.
Use plain attributes where appropriate.
Python does not need Java-style getters/setters.
Bad:
project.get_id()
project.set_id(id_)
for ordinary attributes.
Prefer:
project.id
unless access has real behavior.
Do not use:
self._project_id
plus a property purely for encapsulation theater.
Python's conventions are enough.
Use private-ish fields when they represent internal implementation state.
For Mongo/PyMongo:
Bad:
document: dict[str, Any]
domain = document.get("domain")
if isinstance(domain, ObjectId):
return str(domain)
when the document schema is known.
Use a model/TypedDict:
class DNSDocument(TypedDict):
domain: ObjectId
then:
str(document["domain"])
Or use an ODM model if the project already does.
If SQLAlchemy/Django models define:
project.id: str
project.enabled: bool
do not repeatedly runtime-check them inside business logic.
Trust the ORM model unless the field genuinely allows null.
Bad:
repository.find(
collection="projects",
filters={"id": project_id},
projection=None,
options={}
)
when the application has a clear domain operation.
Prefer:
project_repository.get(project_id)
Do not make internal APIs mimic a generic database driver unless generic behavior is genuinely needed.
Avoid:
Repository
β DAO
β Store
β DatabaseClient
β Session
for simple persistence.
Use the minimum layering that gives useful testability and separation.
Do not build elaborate:
UnitOfWork
TransactionManager
TransactionScope
TransactionProvider
around a small amount of transaction code unless the application genuinely benefits.
Use the ORM/database's native transaction API directly where clearer.
Bad:
class QueueMessage:
event: str
data: dict[str, Any]
with downstream code doing:
if message.event == "domain.map":
data = as_domain_map_event(message.data)
Prefer event-specific parsing at the boundary.
Example:
class DomainMapEvent(BaseModel):
event: Literal["domain.map"]
project_id: str
domain: str
Or use discriminated unions if the project's validation library supports them.
Parse JSON once.
Validate once.
Convert into the domain representation once.
Do not repeatedly do:
json.loads(...)
dict(...)
model_validate(...)
asdict(...)
across layers.
Bad:
data = json.loads(model.model_dump_json())
or:
payload = json.loads(json.dumps(data))
just to convert structures.
Use direct conversions or actual typed objects.
With Pydantic, do not constantly turn models back into raw dicts:
service.run(payload.model_dump())
if the service could accept the model/type directly.
Dump only at serialization or integration boundaries.
Bad:
service.create_project({
"project_id": project_id,
"region": region,
})
when:
service.create_project(project_id, region)
or a meaningful request object is clearer.
Use parameter objects when the values form a real concept or the parameter list is substantial.
Do not pass huge context objects where only two fields are needed.
Bad:
def sync_project(payload: ProjectPayload) -> None:
project_id = payload.project_id
rate_limit = payload.rate_limit
if only those fields matter.
Prefer:
def sync_project(project_id: str, rate_limit: int) -> None:
unless the payload itself is the meaningful domain concept.
Bad:
def create_project(options: dict[str, Any]) -> Project:
Prefer explicit parameters or a concrete model.
Generic option dictionaries push validation problems downstream.
Bad:
deploy(
project,
force=True,
skip_cache=False,
async_mode=True,
validate=False,
)
If flags create meaningfully different operations, consider separate functions or a clear options model.
Do not create cryptic combinations of booleans.
At the same time, do not turn:
deploy(project_id)
into:
DeployCommand(
options=DeployOptions(
behavior=DeployBehavior(...)
)
)
without a real reason.
Avoid both extremes.
Avoid:
logger.debug("Entering function")
logger.debug("Validating input")
logger.debug("Calling repository")
logger.debug("Repository returned")
logger.debug("Returning result")
Log:
- failures
- important state transitions
- external operations
- debugging information with real operational value
Do not narrate code execution.
If the codebase uses structured logging:
logger.info(
"project deployed",
extra={"project_id": project_id},
)
follow existing conventions.
Do not introduce a new logging framework during cleanup.
Delete comments like:
if project is None:
domain = domain.lower()
Keep comments for:
- business rules
- quirks
- invariants
- external constraints
- non-obvious reasoning
Comments should explain why.
Bad:
def get_project(project_id: str) -> Project:
"""Get a project."""
This adds nothing.
Keep docstrings for public APIs, complex semantics, parameters with non-obvious contracts, or behavior worth documenting.
Do not add huge Google/Numpy-style docstrings to obvious private helpers.
Example of unnecessary ceremony:
def normalize_domain(domain: str) -> str:
"""
Normalize the domain name.
Args:
domain: The domain name.
Returns:
The normalized domain name.
"""
Prefer self-explanatory code.
Apply anti-slop rules to tests too.
Be suspicious of:
TestDataBuilder
MockFactory
FixtureFactory
ScenarioBuilder
BaseTestCase
IntegrationTestHelper
for simple tests.
Prefer direct setup and pytest fixtures where useful.
Do not mock pure internal code unnecessarily.
Mock real external boundaries where isolation matters:
- network
- filesystem
- third-party APIs
- expensive infrastructure
Use real domain objects for internal logic when practical.
Good:
@pytest.mark.parametrize(
("value", "expected"),
[
("EXAMPLE.COM", "example.com"),
(" example.com ", "example.com"),
],
)
def test_normalize_domain(value: str, expected: str) -> None:
assert normalize_domain(value) == expected
Do not build a testing DSL for three cases.
Do not create:
BaseServiceTest
BaseRepositoryTest
BaseIntegrationTest
unless there is substantial common lifecycle behavior.
Prefer fixtures and composition.
Bad:
def safe_mock_return(mock: Mock, default=None):
Use the mocking library normally.
Do not invent abstraction around standard test tooling without need.
Do not remove validation for:
- HTTP requests
- CLI arguments
- queue payloads
- config/env vars
- webhooks
- untrusted JSON
- external APIs
- schemaless database records
- user input
- file contents
That is where defensive programming belongs.
Do not remove:
- network timeout handling
- database errors
- not-found cases
- transaction rollback
- file-not-found handling
- permission errors
- subprocess failures
- cancellation handling
- API-specific exceptions
- security checks
This is not a request to make code fragile.
The distinction is:
Defend against external uncertainty, not against correctly typed internal code.
This is not a cleanup:
value = cast(str, data["domain"])
becoming:
value = data.get("domain")
if value is None:
return ""
if not isinstance(value, str):
return ""
if len(value) == 0:
return ""
return value
if the actual contract says domain is required and is a string.
The correct fix is:
@dataclass
class DomainEvent:
domain: str
and then:
event.domain
Whenever you see:
safe_x
as_x
normalize_x
extract_x
convert_x
resolve_x
ensure_x
coerce_x
trace the value upstream.
Ask:
- Why is this value not already typed?
- Where does it enter the application?
- Is that where validation belongs?
- Can downstream code receive a concrete type?
- Can the helper disappear entirely?
Always prefer fixing the earliest sensible point in the data flow.
Search the repository for:
Any
dict[str, Any]
Mapping[str, Any]
object
cast(
isinstance(
hasattr(
getattr(
setattr(
vars(
__dict__
inspect.
type(
try:
except Exception
except:
pass
return None
return {}
return []
return ""
.get(
or {}
or []
or ""
deepcopy
Base
Abstract
Mixin
Manager
Processor
Factory
Builder
Resolver
Converter
Mapper
Validator
Helper
Utils
Protocol
ABC
Generic
TypeVar
create_task
gather
to_thread
run_in_executor
retry
lru_cache
Do not automatically remove every match.
Use them as signals to inspect for unnecessary complexity.
Especially review directories/features containing combinations like:
base.py
interfaces.py
protocols.py
factory.py
builder.py
mapper.py
converter.py
validator.py
resolver.py
manager.py
processor.py
helpers.py
utils.py
service.py
repository.py
for one small feature.
Determine whether each layer has real behavior.
Collapse meaningless ones.
Before adding or keeping code, ask:
Is this defending against something that can genuinely happen here?
If no, remove it.
Ask:
Is this complexity caused by an overly broad type?
If yes, fix the type.
Ask:
Does this helper represent a real concept?
If no, inline it.
Ask:
Does this abstraction reduce total complexity?
If no, delete it.
Ask:
Is this dynamic Python because the problem is actually dynamic, or because the code does not know its own types?
If the latter, fix the model.
Ask:
Would plain Python be easier to understand?
If yes, use plain Python.
Prefer:
async def handle_domain_map(
event: DomainMapEvent,
) -> None:
domain = event.domain.strip().lower()
await domain_service.map(
project_id=event.project_id,
domain=domain,
)
over:
async def handle_domain_map(
raw_data: Any,
) -> None:
data = ensure_dict(raw_data)
project_id = safe_string(
data.get("project_id")
)
domain = normalize_value(
data.get("domain")
)
if not project_id or not domain:
return
payload = DomainMappingPayload.from_dict(
{
"project_id": project_id,
"domain": domain,
}
)
await domain_manager.process_mapping(payload)
Before editing Python code:
- Identify the trusted and untrusted boundaries.
- Determine the actual data shapes.
- Check whether
Any/generic dictionaries are necessary. - Understand whether
Noneis genuinely valid. - Inspect whether abstractions have real callers/implementations.
- Prefer fixing data models upstream instead of adding local guards.
- Do not add new helpers before understanding why the existing type is broad.
Before finishing any Python change, inspect the diff.
For every added:
- helper
- class
- protocol
- base class
- mixin
- factory
- builder
- wrapper
- fallback
isinstancegetattrAnydict[str, Any]try/except- nullable type
- decorator
ask:
- Does this handle a state that can genuinely occur?
- Am I compensating for bad typing upstream?
- Could this code be direct instead?
- Did I add another abstraction layer?
- Does this helper have a real reusable concept?
- Am I hiding invalid input behind an empty value?
- Am I swallowing an error?
- Did I introduce dynamic behavior where the shape is known?
- Did the change increase total complexity?
- Could any newly added code simply be deleted?
If yes, simplify before completing the task.
The codebase should trend toward:
- fewer
Anyvalues - fewer generic dictionaries
- fewer runtime type checks
- fewer
.get()chains - fewer silent fallbacks
- fewer broad
except Exception - fewer tiny helper functions
- fewer mapper/converter classes
- fewer base classes and mixins
- fewer factories/builders
- fewer unnecessary protocols
- fewer pass-through layers
- more precise models
- validation concentrated at boundaries
- direct attribute access
- explicit business logic
- idiomatic Python
- easy-to-follow control flow
The important metric is not line count alone.
The important metric is:
Can another engineer understand the behavior without navigating defensive machinery and unnecessary abstractions?
Do not transform:
validated typed input
β direct business logic
into:
Any
β isinstance
β getattr
β safe helper
β converter
β fallback
β wrapper
β manager
β actual operation
The desired flow is:
untrusted input
β parse/validate once
β precise Python type
β straightforward business logic
The overriding principle is:
Make uncertainty explicit at the boundary. Keep trusted Python code simple, typed, direct, and boring.