Appearance
Thoughts on Various Python Features
Let first acknowledge the great aspects of Python.
Python’s human language like syntax has lowered the perceived barrier to entry for beginners. Unlike many other languages (i.e. printf, cout, System.out.println, console.log), print just works. So are the lack of brackets and semicolons, they reduce mental overloads and unfamilarity.
Python has played a crucial role in enabling data scientists. With the de facto libraries like NumPy and pandas, it has established itself as the go-to language for machine learning and data analysis.
One of the most game-changing innovations in Python’s ecosystem is the Jupyter Notebook. By seamlessly integrating code, output (tables & graphs), and explanatory text, Jupyter has revolutionized the way people write reports, document experiments, and share research. Furthermore, hosted environments like Google Colab allow users to run Python code directly in the cloud, making Python – and thus programming – more accessible.
the else clauses are great 👍
The for...else... and try...except...else.... They eliminated the boilerplate code needed to manually creating a flag in many scenarios.
working decorators 👍
Decorators let developers express logic on a different dimension. In a way, they're like C/C++'s macros, but more structured, readable, and integrated into the language. They allow behavior to be layered on top of functions or classes without cluttering the core logic.
There’s valid criticism: decorators hide important logic, make debugging more difficult, and push code toward being overly declarative rather than imperative. But I argue that this “logic hiding” is exactly what makes it powerful. They enable the creation of clean, domain-specific abstractions (and thus DSLs). Like any powerful tool, it comes down to how you use it. Python gives you the choice — and that’s a good thing.
The contrast with JavaScript makes me appreciate this even more. JavaScript decorators have existed through transpilers and frameworks for years, but the standard proposal has gone through multiple incompatible versions and is still at Stage 2.7. Python decorators have been stable since Python 2.4. The syntax is understood by the language, editors, type checkers, and developers alike. Python made the feature boring—and boring language features are usually the ones you can trust.
powerful standard library 👍
Python's standard library is very powerful and it's like a swiss army knife. Some very useful ones are:
loggingrandom.choice .randrange .gaussfunctools.cachecontextlib.suppress
dataclasses 👍
dataclass is another great example of Python adding a feature at the right level. Data-holding classes are common, but manually writing __init__, __repr__, and equality methods is repetitive and distracts from the actual data model.
python
from dataclasses import dataclass
@dataclass(frozen=True)
class Coordinate:
latitude: float
longitude: floatThis gives you a useful constructor, readable output, comparisons, and—because of frozen=True—protection against accidental assignment. It is concise without inventing a second class system, and it works with type hints instead of replacing them.
gamble of whitespace 😐
Python took a bold approach for its block definition syntax: pure indentation. At first glance, it’s a charming idea. For beginners, it removes the confusion around braces and syntax noise.
I argue that the benefits of whitespace stop there. Once you move past the beginner phase, it often becomes more of a annoyance than a feature.
Harder to refactor
Refactoring is a core part of software development—it's how we improve structure, readability, and maintainability without changing behavior. In practice, developers often spend more time reshaping and reorganizing existing code than writing brand new features.
Consider this:
python
def send_notifications(users):
failed = []
for user in users:
if not user.get("email"):
log_warning(f"No email for user {user['id']}")
continue
if user["active"]:
message = f"Hello {user['name']}, you have {len(user.get('updates', []))} new updates."
try:
send_email(user["email"], message)
except EmailError as e:
log_warning(f"Failed to email {user['email']}: {e}")
failed.append(user["id"])
return failedWhen you want to extract logic into a function called notify_user(user), you have to carefully unindent everything, including the try-except block.
Sometimes I like to extract the logic first to see how many variable I need to pass, which might hint me to refactor data structures – and I might not do the logic refactor given this info. This does not work with Python. I have to fix all syntax errors first to get an view of the real errors.
In a real-world refactoring, these micro-frictions add up fast.
Harder to type in interactively
Copy-pasting from codebase into console is also frustrating.
Using the same example, I instinctively paste these into the console, which, of course, gives a IndentationError: unexpected indent
python
message = f"Hello {user['name']}, you have {len(user.get('updates', []))} new updates."
try:
send_email(user["email"], message)
...I have to temporarily unindent the lines in my editor before copying.
Less curly brackets, but more parentheses needed
Consider this:
python
def get_full_name(first, middle, last):
name = (
first + ' ' +
middle + ' ' +
last
)
return nameWhere in many other languages, you can just do this, no parens and also saving 2 lines worth of vertical space.
js
name = first + ' ' +
middle + ' ' +
lasttyping and type checking 🤢
Static typing is critical in larger codebases, they’re a lifeline for readability, documentation, editor support, and, empowers LLM through LSP. Types are no longer just for humans and linters; they're a bridge to intelligent tooling that can reason about code structure at scale.
But the reality is, Python wasn’t designed with any of that in mind.
Python was never meant to be statically typed. The type hints added in Python 3.5 and beyond are just retrofits.
Specifying anything beyond the most basic types in Python can quickly become a chore. While simple annotations like int, str, or list[str] are easy to read and write, things start to unravel the moment you reach for more expressive constructs.
Take Union types, for example. Before Python 3.10, you had to write Union[int, str], which is already clunky—but once you start nesting unions, generics, and optionals, readability drops off a cliff. The new int | str syntax helps.
Some common pain points include:
Callablesyntax: Want to type a function that takes twoints and returns astr? That’sCallable[[int, int], str]- Poor ergonomics for
OptionalandUnion: They are simply too long to be ergonomic. They clutter the signature and add visual noise. - Generics: are too verbose and difficult to use. 3.12's new Type Parameter Syntax helps.
- Tooling inconsistency:
mypy,pyright, and your IDE might interpret edge cases differently. - Constant importing: Almost every time you write type hints, you need to import something from the
typingmodule—Union,Optional,Callable,TypeVar, and more. And before Python 3.9 (late 2020), even basic things likeList,Dict, andTuplehad to be imported as capitalized generic types:from typing import List, Dict.
list dict comprehension & functional programming 🤢
Look at this: output -> condition -> source 👎
python
flattened = [
z
for x in items
for y in x.children
for z in y.subitems
if z.enabled
]I want to write this: source -> condition -> output 👍
js
const flattened = items
.flatMap(x => x.children)
.flatMap(y => y.subitems)
.filter(z => z.enabled);In fact, python lacks support for functional programming.
Any healthy general-purposed programming language should have reasonable support for both OOP and FP. Python fails at this. Sure, you’ll find map, filter, reduce, and lambda in the language, but they feel bolted on rather than thoughtfully integrated.
Python's lambda is particularly limiting because its body can only be a single expression. The moment a callback needs a statement, an intermediate variable, or basic control flow, it has to become a separately named function. That is often better for readability, but it makes functional composition unnecessarily clumsy.
Compare
js
const strings = ["apple", "", "banana", " ", "cherry", ""];
const nonEmptyStrings = strings.filter(s => s.trim());with
python
strings = ["apple", "", "banana", " ", "cherry", ""]
non_empty_strings = list(filter(lambda s: s.strip(), strings))And good luck getting inferred type with that.
Too many characters to type 😢
This is a group of things that just causes me to type more stuff.
import import import
How many times do I have to import these things.
import datetime
import json
import random
import math
import os
import sys
import base64
import uuid
import reI get that python has a powerful std lib and not everybody needs everything. But you don't need to import these rarely used constructs.
complexmemoryviewpropertyslicefrozensetascii
Named parameter
Keyword arguments are one of Python's better features, but passing existing variables under the same names becomes repetitive:
python
create_user(
name=name,
email=email,
timezone=timezone,
)JavaScript can use object-property shorthand: { name, email, timezone }. Python considered similar name= shorthand in PEP 736, but the proposal was rejected.
Function signatures also accumulate their own punctuation once an API needs precise calling rules:
python
def transform(source, /, *args, encoding="utf-8", **options):
.../, *, *args, and **kwargs are individually defensible, but together they are difficult to explain and easy to forget. Python's friendly syntax becomes much less friendly at the edges of a reusable API.
Dict access syntax
Python makes objects pleasant to traverse but dictionaries noisy, which matters because JSON-like dictionaries are everywhere:
python
user_name = response["data"]["user"]["profile"]["name"]The repeated quotes and brackets make the structure harder to scan than attribute access.
Safe navigation in dict & list
The situation gets worse when a nested value is optional. Python has no equivalent to JavaScript's optional chaining:
js
const userName = response.data?.user?.profile?.name;The usual dictionary workaround is not pretty:
python
user_name = (
response.get("data", {})
.get("user", {})
.get("profile", {})
.get("name")
)It also quietly assumes that every intermediate value is either a dictionary or missing. If one exists but is None, the chain throws an exception. A common operation should not need a mini abstraction every time.
packaging and environment management 🤢
Python's packaging story has improved, especially with pyproject.toml and newer tools such as uv. But the default path is still difficult to explain.
Should a new project use pip and venv, pip-tools, Poetry, PDM, Hatch, or uv? Is the dependency list in requirements.txt, setup.py, setup.cfg, pyproject.toml, or some combination? Which file is the lockfile? How should the Python runtime itself be installed and selected?
Experienced teams answer these questions by standardizing on a toolchain. The frustration is that every team first has to make that choice. Other ecosystems provide one obvious package manager, manifest, lockfile, and command for running project tools. Python has specifications and interoperable tools, but not one boring default workflow.
This is partly an ecosystem problem rather than a language-design problem. From a developer's perspective, however, the boundary does not matter much when pip install works on one machine and produces a different environment on another.
concurrency and async 🤢
CPython's global interpreter lock historically prevented multiple threads from executing Python code at the same time. Threads remain useful for I/O, but CPU-bound Python code generally has to use processes, native extensions, or another workaround to use multiple cores effectively.
PEP 703 adds a path toward running CPython without the GIL, so this story is finally changing. That is encouraging, but it also demonstrates how deeply an early implementation decision can shape an ecosystem for decades.
asyncio solves a different concurrency problem, but brings function coloring with it. Once a low-level operation becomes async, async and await tend to spread through every caller. Libraries often ship parallel sync and async clients, while applications need event-loop plumbing to cross between them.
Python is not uniquely bad here—most languages with async functions have the same problem. It just feels especially awkward in Python because the language's original synchronous APIs are so simple.
other minor inconveniences & inconsistencies
datetime.utcnow()
datetime.utcnow() sounds like it returns a UTC datetime, but it returns a naive datetime with no timezone information attached. This is such an easy trap that the method was finally deprecated in Python 3.12 in favor of datetime.now(timezone.utc).
The replacement is correct, but also considerably longer. This is a recurring Python pattern: a convenient early API turns out to be ambiguous, and the explicit replacement adds more ceremony forever.
Packages and magic names
Python packages lean heavily on special double-underscore names: __init__.py, __name__, __file__, __package__, and __all__. These rules are learnable, but they make the module system feel discovered rather than designed.
__init__.py is especially overloaded. It marks a regular package, executes initialization code, and often becomes a place to re-export the package's public API. Namespace packages made the marker optional in some cases, which added flexibility but also one more rule to remember.
No built-in frozendict
Python has mutable and immutable sequences (list and tuple) and mutable and immutable sets (set and frozenset), but no equivalent immutable dictionary. types.MappingProxyType provides a read-only view over another mapping, but it is not the simple, hashable value type that frozendict would suggest.
It is a minor omission, but an odd one in a language where dictionaries are so central.
Final Words
Despite all this, like what I expressed in the first section, Python is still great, enabling many more people to program, introducing diversity to language design. I might even still recommend Python as a first languge to some people. It is simply not a good fit for software engineering.
