Home Posts I Relearned C After 10 Years of Python: Where My Modern Brain...

I Relearned C After 10 Years of Python: Where My Modern Brain Stumbled

15
0

I wrote my first C program in college, a command line calculator that barely handled division. For years after, I never touched it again. Python became my daily language. I wrote backend services, automation scripts, and machine learning pipelines without ever thinking about memory allocation or pointer arithmetic. Then a side project forced me back to C. I needed to write firmware for an STM32 microcontroller, and the ecosystem spoke C. I thought it would be like riding a bike after a decade away. It was not. My modern brain, shaped by garbage collection and list comprehensions, stumbled in ways I did not expect. This is the story of what I had to unlearn, the specific concepts that broke my Python-trained intuition, and what I gained from the struggle.

Why I Chose to Relearn C Instead of Using a Higher Level Alternative

There were easier paths. MicroPython exists. I could have used Arduino’s C++ wrappers. But I wanted to understand the hardware directly. I had spent ten years building on abstractions that someone else wrote, and I felt a nagging gap in my knowledge. When the firmware project appeared, it felt like the right excuse. I bought a cheap STM32 discovery board, installed the ARM GCC toolchain, and opened a blank main.c file. The first thing I did was try to write a Python style for loop. That was the first stumble, and it set the tone for the weeks that followed.

The First Stumble: Loops and the Missing Range Iterator

In Python, iterating over a sequence is a breath. You write for item in items, and the language handles the rest. In C, there is no iterator protocol. A for loop is a thin wrapper over a counter. I found myself writing clunky index based loops, counting array lengths manually, and off by one errors crept in immediately. I had forgotten that C arrays do not carry their length. The array decays to a pointer, and sizeof no longer gives you what you expect. I spent an evening chasing a bug where I was iterating past the end of an array, reading garbage values, and corrupting nearby memory. Python had taught me to think of collections as safe, bounded objects. C reminded me that an array is just a block of memory with no guardrails. That mental shift was the first and most important lesson.

Memory Management: The Mental Tax I Had Forgotten

Python reclaims memory for you. You create objects and abandon them, and a garbage collector eventually tidies up. In C, every allocation is a decision. I used malloc to grab a buffer for sensor data, and I forgot to call free when the function exited. The firmware ran for an hour, then crashed. I had created a slow memory leak that accumulated with every sensor poll cycle. Debugging this required a logic analyzer and a lot of printf calls. I learned to pair every malloc with a free, not just in the code but in my mental model. More than that, I learned to design my functions around ownership. Who allocates the memory? Who is responsible for freeing it? Python never forced me to ask these questions. C demands clear answers, and my initial answers were wrong more often than not.

Pointers: The Concept I Thought I Understood

I knew what a pointer was. I could explain it in an interview. But using pointers fluently in a real system is a different skill. I needed to pass a large configuration struct to a function without copying it, so I passed a pointer. Then I modified the struct inside the function and expected the caller to see the changes. That worked. But then I passed a pointer to a pointer to a linked list insertion function and got hopelessly tangled. Dereferencing the wrong level gave me a segmentation fault. The compiler gave me a warning I ignored. Python had conditioned me to treat warnings as optional. In C, warnings are often the last signal before a crash. I learned to compile with -Wall -Werror and to never ignore a warning again. Pointers are not hard conceptually, but they require a precision that Python’s object model never demanded.

Strings: The Silent Torture

String handling in Python is a joy. You concatenate, split, and format without thinking about buffers. In C, strings are null terminated character arrays. Concatenating two strings requires manually allocating a buffer large enough for both, copying the first, appending the second, and ensuring the null terminator is placed correctly. I used strcat without checking the destination buffer size and overflowed into adjacent memory. The firmware acted strangely for days, sometimes working and sometimes corrupting sensor readings. The bug was a classic buffer overflow, and it taught me the importance of strncat, snprintf, and defensive string handling. I also realized how much Python’s immutable strings protect you from. In C, a string is just a pointer, and a mistake with that pointer can silently corrupt your entire program state.

The Build System and Dependency Maze

Python projects start with a virtual environment and a requirements file. C projects start with a Makefile, or CMake, or Meson, and the configuration is overwhelming. I had to link the STM32 HAL libraries, set the correct compiler flags for my chip architecture, and manage include paths. The first time I tried to include a header from another directory, the compiler could not find it. I added the path, then the linker complained about undefined symbols. I had to learn the difference between compiling and linking, a distinction Python hid entirely. The build system felt like a separate programming language, and I spent almost as much time wrangling it as writing the firmware logic. I eventually settled on a simple Makefile generated by the STM32CubeMX tool, but I made a note to learn CMake properly for future projects.

Error Handling Without Exceptions

Python uses exceptions. When something goes wrong, an exception propagates up the call stack until something catches it. C uses return codes. Every function returns a value, and you must check it. I forgot to check the return value of HAL_UART_Transmit, and the firmware silently dropped bytes. I forgot to check the return value of malloc, and the firmware crashed when memory was exhausted. Python had trained me to expect failure to be loud. C requires you to check for failure explicitly, and skipping the check means undefined behavior. I developed a discipline of always checking return codes, even when I thought a function could not fail. This discipline felt tedious at first, but it forced me to think about failure modes that I had previously ignored. In Python, I caught exceptions when I remembered. In C, every call is a potential failure point, and the code reflects that reality.

Debugging Without a REPL

Python’s interactive shell is a superpower. You can import a module, call a function, and inspect the result in seconds. C has no equivalent. I relied on printf debugging for the first few weeks, scattering print statements throughout the code and recompiling. This was slow and invasive. Later, I connected a hardware debugger and used GDB to set breakpoints and inspect memory. That was faster but required learning a new tool. The lack of a REPL changed my development flow. I wrote smaller chunks of code, compiled more often, and tested each piece in isolation before integrating. Python had allowed me to be sloppy with incremental testing because the feedback loop was so tight. C forced me to be deliberate. The compile time alone, which was only a few seconds, felt like an eternity compared to Python’s instant execution.

What I Gained That Python Could Not Teach Me

Relearning C did not make me want to abandon Python. It made me a better Python developer. I now understand what happens when I create a list and append to it. I understand why large data structures are better passed by reference. I understand the overhead of exception handling and the cost of dynamic dispatch. I also gained a deep appreciation for the engineers who write the libraries I import. Every Python package I use, from NumPy to Flask, ultimately rests on C code. That code is not magic. It is the product of careful memory management, precise pointer arithmetic, and a build system that someone configured. Knowing C does not mean I will write everything in it. It means I can read the foundations and understand what my high level code is actually asking the machine to do.

What I Would Do Differently

If I were to start this journey again, I would change my approach. I would begin with a smaller project than firmware, perhaps a simple command line tool on my laptop, to practice memory management and build system setup without the added complexity of hardware debugging. I would use a modern C build system from the start, like CMake, rather than hand crafting Makefiles that became unmanageable. I would also invest time in learning GDB early instead of relying on printf. The productivity gain from a proper debugger paid for the learning time many times over. Most importantly, I would pair my C practice with deliberate study of the C standard and the compiler’s behavior, rather than guessing why something broke. C rewards precise understanding more than any language I have used.

Should You Relearn C After Years of Higher Level Languages?

If you write Python, JavaScript, or Ruby every day, I recommend spending a month with C. Not to switch, but to stretch. The concepts that feel like limitations, manual memory, raw pointers, explicit error checking, are the foundations that your comfortable abstractions rest on. Understanding them does not make you a purist who rejects higher level tools. It makes you a developer who can debug a segfault in a native extension, read the source of a slow inner loop, and reason about performance at the hardware level. My modern brain stumbled often, but each stumble taught me something that Python had hidden. The difficulty was the point. C is not a gentle teacher, but its lessons are permanent.

LEAVE A REPLY

Please enter your comment!
Please enter your name here