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.

Vous aggravez cela par un biais de confirmation. Vous savez que vous avez tapé une affectation parce que c'était votre intention. Lorsque vous lisez le code pour la cinquième fois, votre cerveau corrige automatiquement le symbole. C'est pourquoi le rubber ducking fonctionne. Cela vous oblige à articuler chaque ligne assez lentement pour que l'écart entre ce qui est écrit et ce que vous vouliez dire devienne visible.

Les petits bugs de ce type sont plus difficiles à trouver que les plantages spectaculaires. Un segfault ou une erreur de syntaxe s'annonce immédiatement. Un no-op silencieux corrompt simplement l'état et laisse le programme continuer en boitant. L'échec survient en aval, et votre instinct est de déboguer le symptôme plutôt que la cause.

Une meilleure défense

Vous ne pouvez pas vous fier uniquement à vos yeux. Après cet épisode, j'ai changé quelques habitudes qui auraient permis de détecter l'erreur plus tôt.

Premièrement, si vous maintenez un état dans un dictionnaire, envisagez d'utiliser une dataclass ou un enum.Enum pour les états de processus. Définissez vos statuts sous forme de constantes ou de membres d'énumération :

from enum import Enum

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

Avec des types explicites, des outils comme mypy peuvent signaler des comparaisons suspectes lors de l'analyse statique. Une comparaison accidentelle là où devrait se trouver une affectation devient beaucoup plus facile à repérer lorsque les types ne correspondent pas aux attentes.

Deuxièmement, écrivez des tests unitaires pour les transitions d'état avant d'écrire la logique de l'ordonnanceur. Un test simple qui crée un processus avec un seul tick de travail, exécute l'ordonnanceur et vérifie que l'état final est FINISHED aurait échoué immédiatement. Cet échec aurait permis de restreindre la recherche à la logique de mise à jour de l'état plutôt que de me laisser errer dans toute la boucle.