Building an operating system from scratch sounds like a job for kernel hackers writing C. But you can spin up a simplified simulation in Python in an afternoon, and you will quickly discover that the logic of process management is just as unforgiving in a high-level language. I learned this the hard way. I sat down to write a tiny OS simulator. The goal was modest: create a few processes, schedule them, and mark them finished when their work was done. The code was short. The logic felt bulletproof. Then I ran it, and nothing would die.

Why Build a Mini OS in Python?

A real operating system juggles memory paging, file systems, hardware interrupts, and device drivers. A simulation strips all of that away and lets you focus on the core idea: state. You define a process. It has a PID, a burst time, and a lifecycle status. Ready. Running. Finished. A scheduler loop picks the next candidate, advances its state, simulates a time slice, and transitions it to done.

Python is an excellent vehicle for this kind of experiment because it lets you ignore pointer arithmetic and memory alignment. A list of dictionaries becomes your process table. A while loop becomes your kernel scheduler. You can implement round-robin scheduling or priority queues with nothing more than standard library tools. It feels approachable, which is exactly why the bug that followed was so infuriating.

The Setup

My simulation used a list called process_table. Each entry was a dictionary shaped like this:

{
    "pid": 1,
    "burst_time": 3,
    "status": "ready"
}

The scheduler ran a simple while loop. It scanned the table for the first process whose status was not "finished". When it found one, it called a helper function, execute_tick(p), to run that process for one simulated cycle. Inside execute_tick, I set the process status to "running", decremented the burst time, and checked if the remaining work hit zero. If it did, I updated the status to "finished". The outer loop was supposed to terminate once every process reached the finished state.

On paper, the flow was clean. Find a ready process. Run it. Check for completion. Repeat until done. I even added print statements to watch the scheduler do its work. I could see the processes being picked. The loop kept churning. Yet the processes seemed to enter an eternal present tense, forever running, never moving on.

The Symptom

This is the worst kind of failure: the silent kind. No stack trace spat itself into the terminal. No IndexError or KeyError gave me a breadcrumb to follow. The interpreter was perfectly happy. The program simply did not behave. Processes started, but they never finished. I spent hours retracing the flow.

Was the loop condition wrong? Maybe I had an off-by-one error in the burst time calculation. Was the process table getting shadowed or copied instead of updated in place? Was my termination condition checking the wrong key? I added more prints. I audited every boolean expression. I questioned everything except the one line that actually mattered.

The Culprit

Then I saw it. Inside execute_tick, I had written:

p["status"] == "running"

Two equal signs. A comparison, not an assignment. The fix was a single character away:

p["status"] = "running"

In Python, p["status"] == "running" is a perfectly valid expression. It evaluates to True or False, and then the interpreter discards the result because I never assigned it to anything. The line does absolutely nothing useful. The dictionary entry remained untouched, preserving whatever status it held before, and the process never advanced through its lifecycle.

I changed it to a single equals sign. I ran the script again. The simulation breathed. Processes cycled through ready, running, and finished exactly as planned. One extra keystroke had cost me hours.

Why These Bugs Hide

The reason this stings so much is that Python does not flag an expression statement as an error unless it is outright invalid syntax. The bug was a semantic typo. The program compared the status, produced a boolean, and threw it away. Because the comparison itself could return False, the process stayed stuck in its previous state, and the outer loop had no reason to break.

You compound this with confirmation bias. You know you typed an assignment because you intended an assignment. When you read the code for the fifth time, your brain autocorrects the symbol. This is why rubber ducking works. It forces you to articulate each line slowly enough that the gap between what is written and what you meant becomes visible.

Small bugs like this are harder to find than dramatic crashes. A segfault or a syntax error announces itself immediately. A silent no-op simply corrupts the state and lets the program limp forward. The failure is downstream, and your instinct is to debug the symptom rather than the cause.

A Better Defense

You cannot trust your eyes alone. After this episode, I changed a few habits that would have caught the mistake earlier.

First, if you are maintaining state in a dictionary, consider using a dataclass or an enum.Enum for process states. Define your statuses as constants or enum members:

from enum import Enum

class ProcessState(Enum):
    READY = "ready"
    RUNNING = "running"
    FINISHED = "finished"

With explicit types, tools like mypy can flag suspicious comparisons during static analysis. An accidental comparison where an assignment should live becomes much easier to spot when the types do not align with expectations.

Second, write unit tests for state transitions before you write the scheduler logic. A simple test that creates a process with one tick of work, runs the scheduler, and asserts the final state is FINISHED would have failed immediately. That failure would have narrowed the search to the state update logic rather than letting me wander through the entire loop