Skip to content

Instantly share code, notes, and snippets.

@pipethedev
Created September 10, 2026 10:50
Show Gist options
  • Select an option

  • Save pipethedev/5707c47edd0d6994347adef72c533de9 to your computer and use it in GitHub Desktop.

Select an option

Save pipethedev/5707c47edd0d6994347adef72c533de9 to your computer and use it in GitHub Desktop.
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.

Python Anti-Slop

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.


1. Remove unnecessary Any

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.


2. Do not create generic conversion helpers

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.


3. Stop accepting Any for convenience

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."


4. Remove fake runtime type safety

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().


5. Do not replace a simple cast with runtime ceremony

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.


6. Prefer actual exception types

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.


7. Avoid catch-all except Exception

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.


8. Never use bare except

Avoid:

try:
    ...
except:
    pass

except at extremely deliberate process-level boundaries.

Bare except also catches system-exiting exceptions.

Use specific exception types.


9. Do not catch and immediately re-raise

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.


10. Avoid swallowing errors

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.


11. Do not log and re-raise at every layer

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.


12. Avoid fallback-to-empty-value programming

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.


13. Do not use .get() everywhere defensively

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.


14. Remove deeply nested .get() chains

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.


15. Avoid generic dictionary plumbing

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.


16. Use TypedDict only where dictionaries are actually appropriate

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.


17. Use dataclasses for data, not ceremony

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.


18. Avoid Pydantic everywhere

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.


19. Validate once

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.


20. Remove isinstance ceremony for known values

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.


21. Do not use hasattr() to avoid understanding a 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.


22. Avoid getattr(..., default) everywhere

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.


23. Avoid reflection-like code

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.


24. Do not use inspect as a substitute for design

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.


25. Avoid universal serializers

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.


26. Fix the source type instead of adding extractors

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.


27. Avoid helper-function explosion

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.


28. Avoid fake separation of concerns

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.


29. Avoid Java-style service architecture

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.


30. Remove pass-through methods

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.


31. Avoid pointless base classes

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.


32. Prefer composition over inheritance

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.


33. Avoid mixin slop

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.


34. Avoid unnecessary protocols and ABCs

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."


35. Do not add interfaces purely for testing

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.


36. Avoid unnecessary factories

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.


37. Avoid builder patterns

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.


38. Avoid excessive classmethods as constructors

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.


39. Remove mapper slop

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.


40. Avoid excessive DTO duplication

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.


41. Avoid dependency injection containers

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.


42. Avoid service locators and globals

Bad:

repo = services.get("project_repository")

Prefer explicit dependencies.

Likewise, avoid mutable module-level globals for application services/config where explicit wiring is practical.


43. Avoid singleton ceremony

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.


44. Avoid context-manager abstraction when with is already enough

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.


45. Use context managers where they matter

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.


46. Avoid blanket retry decorators

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.


47. Avoid decorator overuse

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.


48. Avoid custom decorators for trivial code

Bad:

@ensure_not_none
def process_project(...):

when:

if project is None:
    raise ProjectNotFound(...)

is clearer.

Explicit logic is often better than decorator magic.


49. Prefer early returns

Bad:

if project is not None:
    if project.enabled:
        if project.status == "active":
            # 50 lines

Prefer:

if project is None:
    raise ProjectNotFound(project_id)

if not project.enabled:
    return

if project.status != "active":
    return

# main logic

Keep the happy path obvious.


50. Avoid needless boolean comparisons

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.


51. Avoid nested ternary expressions

Bad:

value = "a" if active else "b" if enabled else "c"

Prefer normal control flow.

Do not compress logic at the expense of readability.


52. Avoid useless temporary variables

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.


53. Avoid list-copying "for safety"

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.


54. Avoid dictionary-copying "for safety"

Likewise:

return dict(config)

should have a concrete ownership reason.

Do not copy mutable structures mechanically.


55. Avoid unnecessary deepcopy

Search for:

copy.deepcopy(...)

Deep copying can be expensive and usually signals unclear ownership.

Use it only when nested mutation isolation is actually required.


56. Avoid premature caching

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.


57. Avoid premature async

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.


58. Avoid unnecessary task creation

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.


59. Do not fire-and-forget casually

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.


60. Avoid unnecessary asyncio.gather

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.


61. Do not add locks preemptively

Avoid:

asyncio.Lock()
threading.Lock()

without real shared mutable state and concurrency.

Locks create lifecycle and deadlock complexity.

Protect actual races, not hypothetical ones.


62. Avoid thread pools for normal async I/O

Do not wrap already-async libraries in:

asyncio.to_thread(...)
run_in_executor(...)

without need.

Use thread offloading for genuinely blocking operations.


63. Avoid premature multiprocessing

Do not introduce workers/process pools for small CPU work without evidence.

Measure first.

Simple code first.


64. Do not create generic result wrappers

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.


65. Avoid Optional everywhere

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.


66. Avoid defensive None checks everywhere

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.


67. Avoid or default when falsey values are valid

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.


68. Avoid arbitrary coercion

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.


69. Use enums only where useful

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.


70. Avoid overusing custom value objects

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.


71. Avoid custom container classes

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.


72. Prefer standard library functionality

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.


73. Avoid utility dumping grounds

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.


74. Avoid unnecessary modules

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.


75. Keep modules focused but not fragmented

A module can contain several closely related functions.

Do not interpret "single responsibility" as "one function per file."

Optimize for discoverability.


76. Avoid magic registries

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.


77. Prefer direct dispatch

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.


78. Avoid strategy-pattern slop

Three branches do not automatically need:

BaseStrategy
InsertStrategy
DeleteStrategy
ReplaceStrategy
StrategyFactory

Use ordinary Python control flow when easier to understand.


79. Avoid unnecessary lambdas

Bad:

processor(value, lambda x: normalize(x))

when:

processor(value, normalize)

is enough.

Or simply:

normalize(value)

if the abstraction itself is unnecessary.


80. Avoid clever comprehensions

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.


81. Avoid one-line cleverness

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.


82. Avoid overusing functional programming helpers

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.


83. Remove redundant list conversions

Bad:

list(map(lambda x: x.id, projects))

Prefer:

[project.id for project in projects]

Use idiomatic Python.


84. Remove needless wrappers around built-ins

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.


85. Avoid regular expressions when string methods suffice

Bad:

re.sub(r"^\s+|\s+$", "", domain)

Prefer:

domain.strip()

Use regex for regex-shaped problems.


86. Avoid eval / exec

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.


87. Avoid monkey patching

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.


88. Avoid metaclass slop

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.


89. Avoid descriptors unless necessary

Likewise, do not introduce custom descriptors for basic validation or computed fields when properties/dataclasses/models solve the problem more clearly.


90. Avoid property ceremony

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.


91. Avoid getters and setters

Bad:

project.get_id()
project.set_id(id_)

for ordinary attributes.

Prefer:

project.id

unless access has real behavior.


92. Avoid unnecessary private-name ceremony

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.


93. Keep database models typed

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.


94. Do not duplicate ORM guarantees

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.


95. Avoid generic repository APIs

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.


96. Do not over-wrap ORM/database calls

Avoid:

Repository
→ DAO
→ Store
→ DatabaseClient
→ Session

for simple persistence.

Use the minimum layering that gives useful testability and separation.


97. Avoid transactional abstractions without need

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.


98. Avoid generic queue payloads

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.


99. Do not repeatedly parse the same payload

Parse JSON once.

Validate once.

Convert into the domain representation once.

Do not repeatedly do:

json.loads(...)
dict(...)
model_validate(...)
asdict(...)

across layers.


100. Do not serialize and deserialize just to change types

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.


101. Avoid model dumping everywhere

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.


102. Avoid unnecessary dictionaries for function parameters

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.


103. Narrow function signatures

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.


104. Avoid option dictionaries

Bad:

def create_project(options: dict[str, Any]) -> Project:

Prefer explicit parameters or a concrete model.

Generic option dictionaries push validation problems downstream.


105. Avoid boolean-flag-heavy APIs

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.


106. Avoid unnecessary enums/config objects for one call

At the same time, do not turn:

deploy(project_id)

into:

DeployCommand(
    options=DeployOptions(
        behavior=DeployBehavior(...)
    )
)

without a real reason.

Avoid both extremes.


107. Keep logging meaningful

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.


108. Prefer structured logging if already present

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.


109. Remove obvious comments

Delete comments like:

# Check if project exists
if project is None:
# Convert domain to lowercase
domain = domain.lower()

Keep comments for:

  • business rules
  • quirks
  • invariants
  • external constraints
  • non-obvious reasoning

Comments should explain why.


110. Avoid docstrings that merely repeat names

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.


111. Avoid AI-generated verbose docstrings

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.


112. Avoid test-helper explosion

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.


113. Avoid mocking everything

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.


114. Prefer straightforward pytest parametrization

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.


115. Avoid test inheritance hierarchies

Do not create:

BaseServiceTest
BaseRepositoryTest
BaseIntegrationTest

unless there is substantial common lifecycle behavior.

Prefer fixtures and composition.


116. Do not create defensive wrappers for test mocks

Bad:

def safe_mock_return(mock: Mock, default=None):

Use the mocking library normally.

Do not invent abstraction around standard test tooling without need.


117. Preserve real boundary validation

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.


118. Preserve real error handling

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.


119. Do not replace one kind of slop with another

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

120. Fix the root cause

Whenever you see:

safe_x
as_x
normalize_x
extract_x
convert_x
resolve_x
ensure_x
coerce_x

trace the value upstream.

Ask:

  1. Why is this value not already typed?
  2. Where does it enter the application?
  3. Is that where validation belongs?
  4. Can downstream code receive a concrete type?
  5. Can the helper disappear entirely?

Always prefer fixing the earliest sensible point in the data flow.


121. Search patterns to audit

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.


122. Be suspicious of AI architecture

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.


123. Apply the anti-slop test

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.


124. Desired style

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)

125. Mandatory before-change review

Before editing Python code:

  1. Identify the trusted and untrusted boundaries.
  2. Determine the actual data shapes.
  3. Check whether Any/generic dictionaries are necessary.
  4. Understand whether None is genuinely valid.
  5. Inspect whether abstractions have real callers/implementations.
  6. Prefer fixing data models upstream instead of adding local guards.
  7. Do not add new helpers before understanding why the existing type is broad.

126. Mandatory after-change review

Before finishing any Python change, inspect the diff.

For every added:

  • helper
  • class
  • protocol
  • base class
  • mixin
  • factory
  • builder
  • wrapper
  • fallback
  • isinstance
  • getattr
  • Any
  • dict[str, Any]
  • try/except
  • nullable type
  • decorator

ask:

  1. Does this handle a state that can genuinely occur?
  2. Am I compensating for bad typing upstream?
  3. Could this code be direct instead?
  4. Did I add another abstraction layer?
  5. Does this helper have a real reusable concept?
  6. Am I hiding invalid input behind an empty value?
  7. Am I swallowing an error?
  8. Did I introduce dynamic behavior where the shape is known?
  9. Did the change increase total complexity?
  10. Could any newly added code simply be deleted?

If yes, simplify before completing the task.


127. Final desired result

The codebase should trend toward:

  • fewer Any values
  • 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?


128. Overriding rule

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment