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



This content originally appeared on DEV Community and was authored by Aaron Steers

As engineers, many of us migrate between languages. Fun fact: 20 years ago 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 “.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.

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 are not particularly strong at dependency injection (DI) without additional frameworks (e.g. Spring, Dagger, Guice, etc.). Unbenownced to many, DI in Python is actually super trivial. To manage dependencies, Java developers writing Python often build layers of abstractions, factories, and injection systems where none are 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 doesn’t need to.

The Pythonic alternative

  • Prefer passing simple functions, Callables, or objects directly.
  • 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())

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

2. The “Everything Must Be a Class” Habit

The Javamindset

In Java, almost 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 class in Python, which feels unnecessary.

The Pythonic alternative

  • Write standalone functions when a class is not 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))

Again, the second version is shorter, more natural, and feels more at home in Python.

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 fail for code review and also for AI agent applications: the amount of context needed to review or to create new code is much higher if you need to fully understand a class’s structure. Compared versus calling a helpful function: there’s no “wrong way” to call a helper function and you only need to read the docstring and function signature to confirm the 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()

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

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)

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

5. Null Checks Everywhere Instead of Leveraging Python Idioms

The Java mindset

Explicit null checks are common.

How it leaks into Python

You’ll often see long if/else blocks guarding against None.

The Pythonic alternative

  • Use default values.
  • Leverage truthiness, or, and dictionary .get() idioms.

Example:

# Java-style mindset in Python
if value is None:
    result = "default"
else:
    result = value

# Pythonic mindset
result = value or "default"

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 keyword arguments or dataclasses instead of builders.
  5. Embrace Python’s idioms for handling None.

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.


This content originally appeared on DEV Community and was authored by Aaron Steers