Why Your Code Works But Still Feels Wrong, And What to Do About It
I used to think working code was good code.
Then a senior engineer reviewed one of my Python scripts, ran it successfully, watched it produce the exact output we needed, and then looked at me and said, “This is going to be a problem in six months.” He was right. I just didn’t know it yet.
That moment cracked something open for me. Because up until then, I was solving the wrong problem. I was asking “does it work?” when the real question is “can anyone including future me trust, extend, and maintain this?”
The gap between code that runs and code that’s genuinely well-written is where most intermediate developers get stuck. And closing that gap is not about memorizing rules. It’s about building instincts that come from pain.
Here are the lessons that cost me the most to learn.
1. You’re mutating state when you shouldn’t be
The first time I spent three hours debugging a function that “worked fine in isolation,” I learned about shared mutable state the hard way. The function wasn’t broken. Everything around it was subtly wrong because something upstream had changed a list I assumed was unchanged.
# Dangerous: mutates the original list
def add_default(items, value=[]):
value.append(items)
return value
print(add_default("apple")) # ['apple']
print(add_default("banana")) # ['apple', 'banana'] -- surprise
Mutable default arguments in Python are evaluated once, not per call. They persist between function calls. The fix is simple:
def add_default(items, value=None):
if value is None:
value = []
value.append(items)
return value
But the real lesson isn’t syntax. It’s learning to treat mutation as a liability, not a convenience.
2. Your functions are doing too much
A function named process_data() that reads a file, parses it, filters rows, and writes output to a database is not a function. It’s a small program disguised as one.
Pro tip: If you can’t describe what a function does in one sentence without using the word “and,” it’s doing too much.
When I started treating functions as single-responsibility units, my debugging time dropped dramatically. Not because the code was simpler, but because when something broke, I knew exactly which layer to look at.
# Instead of one mega-function:
raw = read_csv("data.csv")
clean = filter_nulls(raw)
result = transform(clean)
save_to_db(result)
Four functions. Four responsibilities. Four places to write tests. One obvious place to look when something breaks.
3. You’re writing comments to explain what, instead of why
Most comments I wrote in my first two years of Python were noise:
# Loop through users
for user in users:
...
That comment tells me nothing I can’t read from the code itself. What I actually needed to document was intent:
# Users from legacy system may have null emails — skip to avoid downstream failures
for user in users:
if not user.get("email"):
continue
The second version earns its existence. Code tells you how. Comments should tell you why the business logic, the edge case, or the assumption that isn’t obvious.
4. You’re not handling failure paths
Most Python developers are excellent at writing the happy path. The data is clean, the API responds, and the file exists. But production does not care about the happy path.
import requests
def get_user(user_id):
response = requests.get(f"https://api.example.com/users/")
return response.json()
This function will explode the moment the API is down, the user doesn’t exist, or the response isn’t JSON. A function that only works when everything goes right is not production code. It’s a prototype.
def get_user(user_id):
try:
response = requests.get(f"https://api.example.com/users/", timeout=5)
response.raise_for_status()
return response.json()
except requests. exceptions.RequestException as e:
print(f"Request failed: {e}")
return None
The improvement isn’t just defensive programming, it’s respect for the people who will run this code at 2 AM.
5. You’re using bare except clauses
Catching all exceptions is not error handling. It’s error hiding.
# This swallows everything, including keyboard interrupts
try:
do_something()
except:
pass
I’ve watched this pattern suppress a real bug for weeks because the error was silently eaten every single time. Always be specific about what you’re catching, and always log it.
6. Your naming is too clever
I went through a phase where I named variables things like tmp2, res_f, and df_v3_final_FINAL. Every variable name felt meaningful in the moment. None of them meant anything three weeks later.
The best variable names are boring. They tell you exactly what the thing is without requiring context. filtered_users beats fu. monthly_revenue beats mr. is_active beats flag.
Clever names are a form of technical debt. They compound interest.
7. You’re not using Python’s built-ins
One of the clearest signals that code was written by someone earlier in their Python journey is a manual loop where a built-in would do:
# Manual and verbose
total = 0
for num in numbers:
total += num
# Pythonic
total = sum(numbers)
This applies to any(), all(), zip(), enumerate(), map(), filter(), and a dozen others. These aren’t shortcuts, they’re the idiomatic way to express intent in Python. Using them signals that you understand the language, not just the syntax.
8. You’re not writing for the reader, you’re writing for the machine
The machine doesn’t care how your code reads. The human maintaining it in six months does.
Code is read far more often than it is written. Every refactor, every review, every debugging session is an act of reading. Writing code with the reader in mind explicit variable names, small focused functions, and consistent structure is not a soft skill. It’s an engineering discipline.
9. You’re skipping the automation step
The most expensive mistake I made in my early projects was solving the same problem manually, repeatedly, because automating it felt like overhead. It was not overhead. It was the actual work.
import os
import shutil
def organize_files(source_dir, category_rules):
for filename in os.listdir(source_dir):
for category, extensions in category_rules.items():
if filename.endswith(tuple(extensions)):
dest = os.path.join(source_dir, category)
os.makedirs(dest, exist_ok=True)
shutil.move(os.path.join(source_dir, filename), dest)
rules = {"images": [".png", ".jpg"], "docs": [".pdf", ".docx"]}
organize_files("/Users/me/Desktop", rules)
Twenty lines. Saves twenty minutes every week. After a year, that’s over sixteen hours returned to you.
The question to ask about any repetitive task is not “Can I automate this?” It’s “how quickly can I automate this?” The answer, with Python, is usually faster than you think.
My Thought
Working code is the minimum. The goal is code that communicates clearly, fails gracefully, and can be handed to someone else without a lengthy explanation.
None of these lessons came from reading documentation. They came from breaking things, from code reviews that stung a little, and from returning to old code six months later and not recognizing my own thinking.
That discomfort is where the actual learning happens. Lean into it.
Drop your questions in the comments.
- My Code Diary



