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
orattrs
.
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:
- Don’t over-engineer dependency injection—Python’s simplicity usually covers most use cases.
- Don’t create classes for everything—standalone functions are fine (and often preferred).
- Skip unnecessary interfaces—use duck typing or protocols only if truly needed.
- Use keyword arguments or dataclasses instead of builders.
- 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