Building Tools with unPython: When Python Conventions Get in the Way

From Python to unPython — A Minimalist Approach to Coding

Modern Python encourages readable, expressive code with many high-level conveniences. But those conveniences can sometimes hide complexity, introduce subtle performance costs, or make reasoning harder in large systems. “unPython” is a deliberately minimalist approach: favoring explicitness, simplicity, and predictable behavior by avoiding certain Python idioms and runtime magic. This article explains the philosophy, gives concrete rules, and shows examples you can apply today.

Why unPython?

  • Clarity over cleverness: Minimizing implicit behavior reduces cognitive load for readers and maintainers.
  • Predictability: Fewer language tricks mean fewer surprising interactions and edge cases.
  • Easier reasoning about performance and memory: Explicit data structures and control flow make bottlenecks visible.
  • Interoperability: Simpler code integrates more straightforwardly with other languages, tooling, and static analysis.

Core principles

  1. Explicit is better than implicit — take the Zen literally.
  2. Prefer simple data structures over heavy abstractions.
  3. Keep functions small and focused; avoid large polymorphic functions.
  4. Avoid runtime metaprogramming except when justified and well-tested.
  5. Make side effects obvious and minimize global mutable state.
  6. Favor composition of small functions rather than deep inheritance hierarchies.
  7. Document invariants and contracts; use types where they help.

Practical rules (do/don’t)

  • Do use plain classes or dataclasses for structured data; don’t hide data access behind complex property logic unless necessary.
  • Do prefer tuples or namedtuples/dataclasses for immutable records; don’t overuse mutable nested dicts.
  • Do write small pure functions; don’t make huge functions controlled by many flags.
  • Do prefer list/dict comprehensions for simple transforms; don’t chain long generator pipelines that obscure intermediate state.
  • Do use explicit error handling with clear exceptions; don’t rely on broad except: blocks.
  • Do add type hints for public APIs and complex logic; don’t assume type hints replace tests.
  • Do avoid metaclasses, heavy decorators, and dynamic attribute injection unless they give clear, measured benefits.
  • Do isolate I/O and side effects; don’t sprinkle I/O across business-logic functions.

Examples

  1. Replace clever one-liners with explicit steps
  • Pythonic one-liner:
    result = {k: v for k, v in enumerate(map(transform, filter(pred, items)))}
  • unPython version (clear, testable):
    filtered = [x for x in items if pred(x)]transformed = [transform(x) for x in filtered]result = {i: v for i, v in enumerate(transformed)}
  1. Prefer explicit loops when they improve readability and debuggability
  • Pythonic compressed:
    totals = defaultdict(int)for user, amount in payments: totals[user] += amount

    (This is fine; keep it.)

  • Avoid overly abstracted accumulation using obscure functools.reduce chains unless they actually clarify intent.
  1. Limit magic in classes
  • Avoid:
    • Dynamic attribute creation via getattr/setattr unless implementing a clear proxy.
    • Metaclasses for simple configuration that can be done by composition.
  • Prefer:
    @dataclassclass User: id: int name: str
  1. Error handling: prefer explicit, narrow exceptions
  • Avoid:
    try: do_many_things()except: handle_error()
  • Prefer:
    try: do_many_things()except SpecificError as e: handle_error(e)
  1. Use types to document intent
  • For public functions:
    def compute_score(items: list[str]) -> float: …

When to break the rules

unPython is pragmatic, not dogmatic. Use Pythonic conveniences when they:

  • Provide large, well-understood benefits (e.g., comprehensions for concise transforms).
  • Are supported by tests and code reviews.
  • Improve performance measurably or reduce boilerplate without sacrificing clarity.

Examples where Python features are still useful:

  • Context managers for resource handling.
  • Generators for streaming large datasets.
  • Decorators for clear cross-cutting concerns when simple and well-documented.

Migration checklist (how to apply unPython to an existing codebase)

  1. Identify hotspots: modules with high churn, frequent bugs, or complex logic.
  2. Add tests around current behavior to preserve correctness.
  3. Refactor one module at a time: simplify data structures, split large functions, add types.
  4. Replace metaprogramming with explicit wiring where feasible.
  5. Review for performance regressions and measure.
  6. Enforce style via linters and type checkers; add code review guidance that favors explicitness.

Benefits you’ll notice

  • Faster onboarding for new developers.
  • Easier debugging and fewer subtle bugs from hidden state or magic.
  • More robust interfaces and clearer contracts.
  • Better suitability for static analysis and safer refactoring.

Final thoughts

unPython is about trade-offs: you intentionally give up some of Python’s syntactic sugar and expressive shortcuts to gain clarity, robustness, and predictability in larger systems. Apply its rules where complexity and maintenance risk justify them, and keep Python’s strengths for the cases where convenience and expressiveness win.

If you want, I can convert a short sample module from idiomatic Python to an unPython style as a concrete example.

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *