DEV Community

Aaron Steers
Aaron Steers

Posted on

Escaping Your Java Habits in Python: Writing Clean, Pythonic Code

As engineers, many of us migrate between languages.
Fun fact: 20 years ago - what was the first language I was ever certified in? Java.

But now, a dozen languages later, I want to pull my hair out when I feel like I'm reading Java code inside a file that ends in a ".py" extension.

If you’ve spent significant time in Java, it’s natural to bring those habits along when coding in Python. Unfortunately, some of those habits can lead to over-engineered or awkward code that doesn’t at all feel Pythonic. Worse: your habits may be contributing to bugs, and (worse yet:) slowing down code review.

Not to bring shame, but to improve everyone's lives: I think these patterns are worth calling out — especially for developers making the jump from Java to Python.


1. Overcompensating for Dependency Injection (DI)

The Java mindset

Java is not particularly strong at dependency injection (DI). Without additional frameworks (e.g. Spring, Dagger, Guice, etc.), it's actually super difficult. Unbenounced to many, DI in Python is super trivial, actually. To manage dependencies, Java developers writing Python often build layers of abstractions, factories, and injection systems where none are truly needed.

How it leaks into Python

When we carry this mindset into Python, we often over-engineer DI using generics, abstract base classes, and unnecessary indirection. While Python can do this, it usually isn't needed - and our future selves will thank us if we keep things simple.

The Pythonic alternative

  • Prefer passing simple functions, Callables, and Unions of types.
  • Embrace Python’s duck typing: if an object behaves the way you need, you don’t need to enforce a generic type hierarchy.
  • Use default arguments or keyword arguments for flexibility.

Example:

# Java-style mindset in Python
class DataFetcher(Generic[T]):
    def fetch(self) -> T:
        raise NotImplementedError

class HttpDataFetcher(DataFetcher[str]):
    def fetch(self) -> str:
        return "data"

fetcher: DataFetcher[str] = HttpDataFetcher()
print(fetcher.fetch())

# Pythonic mindset

def fetch_data() -> str:
    return "data"

print(fetch_data())
Enter fullscreen mode Exit fullscreen mode

The second version is shorter, clearer, and easier to maintain.


2. The “Everything Must Be a Class” Habit

The Java mindset

In Java, basically everything lives inside a class. Utility methods go into static classes. Even trivial helpers often get wrapped into objects because free functions aren’t idiomatic.

How it leaks into Python

When carried into Python, we end up with tiny, boilerplate-heavy classes that don’t add real value. For example, you might see a StringUtils or ConnectionBuilder class in Python, which is entirely unnecessary and adds unnecessary friction to your callers.

The Pythonic alternative

  • Write standalone helper functions if a class is not truly needed.
  • Use modules as namespaces (a Python file is already a container).
  • Only create classes when state or behavior needs to be encapsulated, or when instantiating the object adds something meaningful to your workflow.

Example:

# Java-style mindset in Python
class MathUtils:
    @staticmethod
    def add(a, b):
        return a + b

print(MathUtils.add(2, 3))d

# Pythonic mindset

def add(a, b):
    return a + b

print(add(2, 3))
Enter fullscreen mode Exit fullscreen mode

Again, the second version is shorter, more natural, and easier to maintain.

Best yet: the person reading or reviewing the code knows (via static code analysis) that calling the function is correct - no pre-knowledge of how to work with the class is needed in order to review the code. This is an area where Java classically fails in regards to code review and also for AI agent applications: the amount of context needed to review code or to create new code is much higher if you need to fully understand a class's structure.

Compare this with helper functions: there's no "wrong way" to call a helper function; you only need to read the docstring and function signature (aka, use tooltips and Intellisense) to confirm if the function call is correct or not.


3. Overusing Interfaces and Abstract Base Classes

The Java mindset

Every service has an interface, every implementation must be bound to it. This enforces structure, but at the cost of boilerplate.

How it leaks into Python

Developers sometimes mimic this pattern with abstract base classes and heavy use of Generic types, even for simple use cases.

The Pythonic alternative

  • Use duck typing: if an object supports the methods you need, that’s enough.
  • When structure matters, consider typing.Protocol for lightweight contracts.

Example:

# Java-style mindset in Python
class Service(ABC):
    @abstractmethod
    def run(self):
        pass

class PrintService(Service):
    def run(self):
        print("running")

# Pythonic mindset
class PrintService:
    def run(self):
        print("running")

service = PrintService()
service.run()
Enter fullscreen mode Exit fullscreen mode

The second method uses less code, and is easier to support.

🤫 Psst! Don't worry: the type checker will always tell you if you call a method that doesn't exist on the class! Adding type checks to your CI means this is always safe, with often 50% less code and a much more readable and maintainable implementation.


4. Verbose Builders Instead of Simple Keyword Arguments

The Java mindset

Builders are everywhere for object construction with optional arguments.

How it leaks into Python

Developers sometimes reimplement builder-style classes just to avoid long __init__ signatures.

The Pythonic alternative

  • Use keyword arguments with defaults.
  • For structured objects, use dataclasses or Pydantic models.

Example:

# Java-style builder in Python
class UserBuilder:
    def __init__(self):
        self._name = None
        self._age = None

    def set_name(self, name):
        self._name = name
        return self

    def set_age(self, age):
        self._age = age
        return self

    def build(self):
        return {"name": self._name, "age": self._age}

user = UserBuilder().set_name("Alice").set_age(30).build()

# Pythonic with dataclasses
from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int = 0

user = User(name="Alice", age=30)
Enter fullscreen mode Exit fullscreen mode

Again, the Pythonic version requires 75% less code, while being more readable, more maintainable, and much less likely to have unexpected bugs creeping in over time.


5. Not Using Keyword Arguments When You Should

The Java mindset

Function calls are almost always positional (Java doesn't support keyword args), and IDEs enforce correctness through signatures and tooling, while your function and method args inevitable devolve into long list of fragile and hard-to-verify positional inputs.

How it leaks into Python

Ex-Java developers often continue to use positional arguments in Python - even for long, many-input functions. This makes code fragile and unreadable—especially during reviews, where it’s literally impossible to confirm correctness without knowing the implementation signature by heart.

The Pythonic alternative

Use keyword arguments for clarity and maintainability. They make function calls self-documenting and trivial to verify.

Example:

# Fragile and hard to review
create_source(name, config, catalog, state)

# Clear and verifiable
create_source(
    source_name=name,
    config=config,
    catalog=catalog,
    state=state,
)
Enter fullscreen mode Exit fullscreen mode

The latter is obviously correct and clear to its reader, whereas the former is literally impossible to verify from the code alone. The Pythonic version takes <2 seconds to reach 100% confidence (if it passes lint checks: it's correct), whereas in the Java-esque version, there's almost zero ability to reach a high confidence at all. You are fully leaning on tests and type checks - and if any of the arg types are the same, then you are 100% leaning on tests.

🧠 Ironically: even though the Pythonic version has more characters, your eye passes over it faster, quickly and subconsciously confirming the implementation in 0-2 seconds, whereas your brain freezes or just gives up when trying to review the first version. Since you don't have access to docs with the order of the input args, your ability to code review that (admittedly shorter) code snippet is zero-to-nil - unless you really really suspect something is wrong with it.

What's worse: our code changes and evolves over time. Named args ensure your code will break cleanly when it is broken. Relying on positional args is like setting a time bomb that you know will eventually go off, but you just don't know when. 💣


Takeaways

If you’re coming from Java:

  1. Don’t over-engineer dependency injection—Python’s simplicity usually covers most use cases.
  2. Don’t create classes for everything—standalone functions are fine (and often preferred).
  3. Skip unnecessary interfaces—use duck typing or protocols only if truly needed.
  4. Use dataclasses and simple constructors instead of builders.
  5. Prefer keyword arguments in function calls for clarity and reviewability.

The beauty of Python is its flexibility and minimalism. Lean into that. By shedding some habits from Java, your code will not only feel more Pythonic but will also be easier to read, maintain, and extend.

It's not about which language is better - it's about making your code readable, maintainable, and intuitive to leverage and support. In the age of AI, these can mean the difference between keeping up, or falling behind.


Why this matters for the future of AI

In the future, AI will write much more of the code that we all write and maintain today - but it has the same limitations that we have: it is reliant on limited context windows and on context-clues to correctly read and interpret code. If you want to future proof your code in 2025 and 2026: write code that even a mindless robot can review and maintain for you.

In other words, keep it Pythonic - regardless of whether you are writing Python, Java, or Kotlin. 😅

Top comments (0)