DEV Community

TuxAcademy
TuxAcademy

Posted on

Python Did Not Make Me a Better Developer. Debugging Python Did.

I have been writing Python professionally for four years. I have also watched dozens of people learn Python, some who became genuinely capable developers and some who plateaued at tutorial completion and never moved past it.

The difference between those two groups is not intelligence or time invested. It is a specific relationship with error messages.

Let me show you what I mean with actual examples.

The Error That Taught Me How Python Memory Actually Works

Early in my Python journey I wrote something like this:

python
def add_item(item, existing_list=[]):
existing_list.append(item)
return existing_list

print(add_item("apple"))
print(add_item("banana"))
print(add_item("cherry"))

I expected three separate lists. What I got was this:

['apple']
['apple', 'banana']
['apple', 'banana', 'cherry']

My first response was to assume I had made a mistake in the logic. I had not made a mistake in the logic. I had made a mistake in my mental model of Python.

The default argument to a function is evaluated once when the function is defined, not each time the function is called. The empty list I had used as a default was the same list object on every call. Every append was modifying that single shared object.

Reading about mutable default arguments in Python had not taught me this. Watching a tutorial had not taught me this. Encountering this specific behavior, spending twenty minutes confused about it, and then understanding exactly why it happened taught me something about Python object identity and evaluation order that I have never forgotten.

The fix is straightforward:

python
def add_item(item, existing_list=None):
if existing_list is None:
existing_list = []
existing_list.append(item)
return existing_list

print(add_item("apple"))
print(add_item("banana"))
print(add_item("cherry"))
['apple']
['banana']
['cherry']

But the fix is less valuable than the understanding. I now think about object mutability and default argument evaluation every time I write a function with a complex default parameter. That habit came from the confusion, not from the explanation.

The KeyError That Taught Me to Read Data Before Trusting It

Two years into using Python professionally I wrote a data processing script that worked perfectly on my test dataset and failed immediately on production data with:

KeyError: 'customer_id'

The column was clearly named customer_id in the documentation. It was clearly named customer_id in the sample data I had been given. My code referenced customer_id. The error made no sense.

Here is what I should have done first:

python
import pandas as pd

df = pd.read_csv('production_data.csv')

print(df.columns.tolist())
print(repr(df.columns[0]))
print([f"'{col}'" for col in df.columns])

Output:

['customer_id ', 'name', 'email', 'order_count']
"'customer_id '"
["'customer_id '", "'name'", "'email'", "'order_count'"]

There was a trailing space in the column name. It was invisible in normal printing and invisible in the documentation. It existed because the CSV had been exported from a tool that added trailing spaces to some headers and not others.

The fix was one line:

python
df.columns = df.columns.str.strip()

But what I learned was not the fix. What I learned was to never trust data until I have looked at it with functions that show me exactly what is there rather than what appears to be there. repr() on a string shows escape characters and whitespace. tolist() on columns shows me the exact string representation. dtypes shows me whether numbers have been read as strings. describe() shows me if the value ranges are plausible.

I now run these checks on every new dataset before writing a single line of processing code. That habit came from this specific frustrating error, not from any tutorial that told me data could be messy.

The full defensive data loading pattern I use now:

python
import pandas as pd

def load_and_validate(filepath, expected_columns):
df = pd.read_csv(filepath)

df.columns = df.columns.str.strip().str.lower()

print(f"Shape: {df.shape}")
print(f"Columns: {df.columns.tolist()}")
print(f"Data types:\n{df.dtypes}")
print(f"Missing values:\n{df.isnull().sum()}")
print(f"First row:\n{df.iloc[0]}")

missing_cols = set(expected_columns) - set(df.columns)
if missing_cols:
    raise ValueError(f"Missing expected columns: {missing_cols}")

return df
Enter fullscreen mode Exit fullscreen mode

df = load_and_validate(
'production_data.csv',
expected_columns=['customer_id', 'name', 'email', 'order_count']
)

The RecursionError That Taught Me to Think Before Writing

At some point I decided to write a function that flattened a nested list of arbitrary depth:

python
def flatten(nested):
result = []
for item in nested:
if isinstance(item, list):
result.extend(flatten(item))
else:
result.append(item)
return result

This worked fine for normal nested lists. Then I tested it on a list that was nested a few thousand levels deep:

RecursionError: maximum recursion depth exceeded

Python's default recursion limit is 1000. My list exceeded it.

My first instinct was to increase the recursion limit:

python
import sys
sys.setrecursionlimit(10000)

This worked until the list was 10001 levels deep. The real fix was understanding that deeply nested structures should be handled iteratively rather than recursively in Python, because Python does not optimize tail recursion and each recursive call adds a frame to the call stack:

python
def flatten_iterative(nested):
result = []
stack = [nested]

while stack:
    current = stack.pop()

    if isinstance(current, list):
        stack.extend(reversed(current))
    else:
        result.append(current)

return result
Enter fullscreen mode Exit fullscreen mode

deep_list = [1]
for _ in range(5000):
deep_list = [deep_list]

print(flatten_iterative(deep_list))
[1]

The RecursionError taught me that Python is not a language optimized for deep recursion. It taught me to think about stack depth when writing recursive functions and to consider iterative alternatives for problems that could involve deep nesting. No tutorial on recursion in Python had mentioned this practically important limitation with the specificity that encountering the error provided.

The Silent Data Corruption That Taught Me About Floating Point

This one took two days to find and is the error I am most grateful for because it was invisible.

I was writing financial calculations and producing results that were almost correct. Not obviously wrong. Almost correct in a way that only showed up when I compared two values that should have been equal:

python
total_a = 0.1 + 0.2
total_b = 0.3

print(total_a == total_b)
print(total_a)
print(total_b)
False
0.30000000000000004
0.3

This is floating point arithmetic. 0.1, 0.2, and 0.3 cannot be represented exactly in binary floating point. The result of adding 0.1 and 0.2 is not exactly 0.3 in floating point arithmetic.

For most applications this does not matter. For financial calculations where you are comparing values, summing large numbers of small values, or checking whether totals balance, it matters enormously.

The fix for financial calculations is the Decimal type:

python
from decimal import Decimal, ROUND_HALF_UP

price = Decimal('199.99')
tax_rate = Decimal('0.18')
tax = (price * tax_rate).quantize(Decimal('0.01'), rounding=ROUND_HALF_UP)
total = price + tax

print(f"Price: {price}")
print(f"Tax (18%): {tax}")
print(f"Total: {total}")

total_a = Decimal('0.1') + Decimal('0.2')
total_b = Decimal('0.3')
print(f"0.1 + 0.2 == 0.3: {total_a == total_b}")
Price: 199.99
Tax (18%): 36.00
Total: 235.99
0.1 + 0.2 == 0.3: True

I now use Decimal for any financial calculation in Python without exception. That habit came from the two days I spent finding a bug that was invisible in print output but present in the actual values being compared.

The Import Error That Taught Me About Python Environments

Early in my Python journey I installed a library, imported it successfully in one script, and then got ImportError in a different script that should have had access to the same library.

ModuleNotFoundError: No module named 'pandas'

I had pandas installed. I had used it five minutes earlier. What was happening.

What was happening was that I had two Python installations on my machine, one system Python and one that I had installed separately, and pip in one was not pip in the other. When I installed pandas, I installed it for one Python. When I ran the script, it was running with the other Python.

The mental model I needed was virtual environments:

bash
python -m venv project_env

source project_env/bin/activate

pip install pandas numpy matplotlib

python -c "import pandas; print(pandas.version)"

pip freeze > requirements.txt

requirements.txt produced by pip freeze lets any other developer, or your future self on a different machine, recreate exactly the environment your code runs in:

bash
python -m venv new_env
source new_env/bin/activate
pip install -r requirements.txt

I now create a virtual environment for every Python project without exception, before writing the first line of code. That habit came from the ImportError, not from the tutorial that mentioned virtual environments in passing and then moved on.

The Pattern I Noticed Across All of These

Every piece of Python knowledge I would describe as genuinely useful rather than superficially familiar came from an error rather than an explanation.

The mutable default argument behavior: learned from the shared list bug.
Defensive data loading: learned from the trailing space KeyError.
Iterative versus recursive approaches: learned from the RecursionError.
Decimal for financial math: learned from the floating point silent corruption.
Virtual environments: learned from the ImportError.

The pattern is consistent enough that I now deliberately seek out errors rather than trying to avoid them. When I encounter something unfamiliar, I write the naive version first, see what breaks, and then understand why before writing the correct version.

This is slower in the immediate term. It is dramatically faster in the medium term because the understanding developed this way is structural rather than procedural. I understand why the correct approach is correct rather than only knowing that it is.

A Practical Suggestion

If you are learning Python, here is the most useful thing I can tell you from four years of writing it professionally.

For every new concept you encounter, write the version that you think should work before looking at the correct version. Run it. Read the error message in full rather than pattern matching on the first recognizable word. Form a hypothesis about what the error means. Test the hypothesis. Adjust.

This process is what builds the mental model of Python that makes you effective with it, not just familiar with it. The tutorials can tell you what Python does. Only the errors can show you how Python actually works when what you thought it would do and what it does are not the same thing.

The four examples in this post represent maybe thirty hours of frustrated debugging across four years. They also represent the majority of what I actually know about Python that I could not have learned any other way.

The errors were the curriculum. The tutorials were the index.

Resources

For students who want structured guidance through Python from fundamentals to portfolio-ready projects, TuxAcademy's Python program is built around real project work where you encounter real errors and develop real understanding: https://www.tuxacademy.org/courses/programming/python-programming-training-course-greater-noida/

A complete Python interview preparation guide covering the specific questions asked in India in 2026: https://www.tuxacademy.org/python-interview-questions-india-2026/

A complete Python career guide covering the directions Python takes you professionally: https://www.tuxacademy.org/python-career-guide-beyond-programming-india-2026/

Top comments (0)