[{"data":1,"prerenderedAt":394},["ShallowReactive",2],{"blog-\u002Fblog\u002Fpython-cpython-execution-model-explained":3},{"id":4,"title":5,"body":6,"description":380,"difficulty":381,"extension":382,"framework":383,"frameworkSlug":44,"meta":384,"navigation":66,"order":70,"path":385,"qaPath":386,"seo":387,"stem":388,"subtopic":389,"topic":390,"topicSlug":391,"updated":392,"__hash__":393},"blog\u002Fblog\u002Fpython-cpython-execution-model-explained.md","The CPython Execution Model Explained — Bytecode, the Interpreter Loop, and the GIL",{"type":7,"value":8,"toc":370},"minimark",[9,14,28,32,39,132,138,142,153,209,212,216,227,235,242,246,259,265,280,284,291,306,309,313,328,332,366],[10,11,13],"h2",{"id":12},"the-cpython-execution-model-explained","The CPython execution model, explained",[15,16,17,18,22,23,27],"p",{},"\"Python is interpreted\" is only half the story. CPython actually ",[19,20,21],"strong",{},"compiles"," your source to\nbytecode and then runs that bytecode on a virtual machine. Understanding this pipeline —\nsource → bytecode → evaluation loop — demystifies performance, ",[24,25,26],"code",{},".pyc"," files, and the GIL.",[10,29,31],{"id":30},"source-to-bytecode","Source to bytecode",[15,33,34,35,38],{},"When you import or run a module, CPython parses the source into an AST and compiles it to\n",[19,36,37],{},"bytecode",": a sequence of low-level instructions for Python's virtual machine. This is not\nmachine code — it's an intermediate representation the interpreter understands.",[40,41,46],"pre",{"className":42,"code":43,"language":44,"meta":45,"style":45},"language-python shiki shiki-themes github-light github-dark","import dis\n\ndef add(a, b):\n    return a + b\n\ndis.dis(add)\n#   LOAD_FAST   a\n#   LOAD_FAST   b\n#   BINARY_OP   + (add)\n#   RETURN_VALUE\n","python","",[24,47,48,61,68,81,96,101,107,114,120,126],{"__ignoreMap":45},[49,50,53,57],"span",{"class":51,"line":52},"line",1,[49,54,56],{"class":55},"szBVR","import",[49,58,60],{"class":59},"sVt8B"," dis\n",[49,62,64],{"class":51,"line":63},2,[49,65,67],{"emptyLinePlaceholder":66},true,"\n",[49,69,71,74,78],{"class":51,"line":70},3,[49,72,73],{"class":55},"def",[49,75,77],{"class":76},"sScJk"," add",[49,79,80],{"class":59},"(a, b):\n",[49,82,84,87,90,93],{"class":51,"line":83},4,[49,85,86],{"class":55},"    return",[49,88,89],{"class":59}," a ",[49,91,92],{"class":55},"+",[49,94,95],{"class":59}," b\n",[49,97,99],{"class":51,"line":98},5,[49,100,67],{"emptyLinePlaceholder":66},[49,102,104],{"class":51,"line":103},6,[49,105,106],{"class":59},"dis.dis(add)\n",[49,108,110],{"class":51,"line":109},7,[49,111,113],{"class":112},"sJ8bj","#   LOAD_FAST   a\n",[49,115,117],{"class":51,"line":116},8,[49,118,119],{"class":112},"#   LOAD_FAST   b\n",[49,121,123],{"class":51,"line":122},9,[49,124,125],{"class":112},"#   BINARY_OP   + (add)\n",[49,127,129],{"class":51,"line":128},10,[49,130,131],{"class":112},"#   RETURN_VALUE\n",[15,133,134,137],{},[24,135,136],{},"dis"," disassembles a function so you can see the exact instructions. Each line is one\nbytecode operation the VM will execute.",[10,139,141],{"id":140},"code-objects-hold-the-bytecode","Code objects hold the bytecode",[15,143,144,145,148,149,152],{},"The compiled bytecode lives in a ",[19,146,147],{},"code object"," (",[24,150,151],{},"func.__code__","), alongside metadata:\nconstants, variable names, argument count, and flags. A function is essentially a code\nobject plus its defaults and closure.",[40,154,156],{"className":42,"code":155,"language":44,"meta":45,"style":45},"add.__code__.co_code         # the raw bytecode bytes\nadd.__code__.co_consts       # constants used\nadd.__code__.co_varnames     # ('a', 'b')\nadd.__code__.co_argcount     # 2\n",[24,157,158,173,185,197],{"__ignoreMap":45},[49,159,160,163,167,170],{"class":51,"line":52},[49,161,162],{"class":59},"add.",[49,164,166],{"class":165},"sj4cs","__code__",[49,168,169],{"class":59},".co_code         ",[49,171,172],{"class":112},"# the raw bytecode bytes\n",[49,174,175,177,179,182],{"class":51,"line":63},[49,176,162],{"class":59},[49,178,166],{"class":165},[49,180,181],{"class":59},".co_consts       ",[49,183,184],{"class":112},"# constants used\n",[49,186,187,189,191,194],{"class":51,"line":70},[49,188,162],{"class":59},[49,190,166],{"class":165},[49,192,193],{"class":59},".co_varnames     ",[49,195,196],{"class":112},"# ('a', 'b')\n",[49,198,199,201,203,206],{"class":51,"line":83},[49,200,162],{"class":59},[49,202,166],{"class":165},[49,204,205],{"class":59},".co_argcount     ",[49,207,208],{"class":112},"# 2\n",[15,210,211],{},"Code objects are reused — defining a function compiles once, and every call re-runs the same\ncode object with a fresh frame.",[10,213,215],{"id":214},"the-evaluation-loop-and-the-stack","The evaluation loop and the stack",[15,217,218,219,222,223,226],{},"CPython runs bytecode in a giant loop (historically ",[24,220,221],{},"ceval.c",") — a ",[19,224,225],{},"stack-based virtual\nmachine",". Instructions push and pop operands on an evaluation stack.",[40,228,233],{"className":229,"code":231,"language":232},[230],"language-text","# return a + b becomes:\nLOAD_FAST a      # push a onto the stack\nLOAD_FAST b      # push b\nBINARY_OP +      # pop both, push their sum\nRETURN_VALUE     # pop and return\n","text",[24,234,231],{"__ignoreMap":45},[15,236,237,238,241],{},"Each function call creates a ",[19,239,240],{},"frame object"," holding its local variables and its own stack.\nThis is what you see in a traceback — a stack of frames.",[10,243,245],{"id":244},"pyc-files-cache-the-bytecode",".pyc files cache the bytecode",[15,247,248,249,252,253,255,256,258],{},"Compilation isn't free, so CPython caches the bytecode of imported modules in ",[24,250,251],{},"__pycache__","\nas ",[24,254,26],{}," files. On the next import, if the source is unchanged, it loads the ",[24,257,26],{}," and\nskips recompiling.",[40,260,263],{"className":261,"code":262,"language":232},[230],"__pycache__\u002F\n    mymodule.cpython-312.pyc\n",[24,264,262],{"__ignoreMap":45},[15,266,267,268,271,272,274,275,279],{},"The filename encodes the interpreter version, since bytecode is ",[19,269,270],{},"not"," stable across Python\nversions — a ",[24,273,26],{}," from 3.11 won't run on 3.12. Note the ",[276,277,278],"em",{},"top-level"," script you run\ndirectly isn't cached, only imported modules.",[10,281,283],{"id":282},"where-the-gil-fits","Where the GIL fits",[15,285,286,287,290],{},"The ",[19,288,289],{},"Global Interpreter Lock"," protects the interpreter's internal state (including\nreference counts) by ensuring only one thread executes bytecode at a time. It lives at the\nevaluation-loop level: a thread holds the GIL while running bytecode and periodically\nreleases it.",[40,292,294],{"className":42,"code":293,"language":44,"meta":45,"style":45},"# Two threads doing CPU work don't run bytecode simultaneously — the GIL serialises them.\n# Threads DO overlap when one is blocked on I\u002FO, because the GIL is released during the wait.\n",[24,295,296,301],{"__ignoreMap":45},[49,297,298],{"class":51,"line":52},[49,299,300],{"class":112},"# Two threads doing CPU work don't run bytecode simultaneously — the GIL serialises them.\n",[49,302,303],{"class":51,"line":63},[49,304,305],{"class":112},"# Threads DO overlap when one is blocked on I\u002FO, because the GIL is released during the wait.\n",[15,307,308],{},"This is why threads help I\u002FO-bound code but not CPU-bound code, and why true CPU parallelism\nneeds multiple processes. (CPython 3.13 introduced an experimental free-threaded build that\ncan disable the GIL.)",[10,310,312],{"id":311},"cpython-vs-other-implementations","CPython vs other implementations",[15,314,315,316,319,320,323,324,327],{},"CPython is the reference implementation, but the model isn't the only one. ",[19,317,318],{},"PyPy"," uses a\nJIT compiler to turn hot bytecode into machine code for big speedups; ",[19,321,322],{},"Jython"," and\n",[19,325,326],{},"IronPython"," target the JVM and .NET. \"Python the language\" is a spec; CPython is one\n(dominant) way to run it.",[10,329,331],{"id":330},"recap","Recap",[15,333,334,335,337,338,340,341,344,345,348,349,352,353,355,356,358,359,362,363,365],{},"CPython ",[19,336,21],{}," source to ",[19,339,37],{},", stored in ",[19,342,343],{},"code objects",", then executes it on\na ",[19,346,347],{},"stack-based virtual machine"," — the evaluation loop — where each call gets its own\n",[19,350,351],{},"frame",". Imported modules' bytecode is cached as version-stamped ",[24,354,26],{}," files in\n",[24,357,251],{}," to skip recompilation. The ",[19,360,361],{},"GIL"," lives at this level, letting only one\nthread run bytecode at a time (released during I\u002FO), which is why CPU parallelism needs\nprocesses. Use ",[24,364,136],{}," to see the bytecode, and remember CPython is just one implementation of\nthe language.",[367,368,369],"style",{},"html pre.shiki code .szBVR, html code.shiki .szBVR{--shiki-default:#D73A49;--shiki-dark:#F97583}html pre.shiki code .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":45,"searchDepth":63,"depth":63,"links":371},[372,373,374,375,376,377,378,379],{"id":12,"depth":63,"text":13},{"id":30,"depth":63,"text":31},{"id":140,"depth":63,"text":141},{"id":214,"depth":63,"text":215},{"id":244,"depth":63,"text":245},{"id":282,"depth":63,"text":283},{"id":311,"depth":63,"text":312},{"id":330,"depth":63,"text":331},"How CPython actually runs your code — compilation to bytecode, code objects and the evaluation loop, the stack-based virtual machine, .pyc caching, and where the GIL fits in.","hard","md","Python",{},"\u002Fblog\u002Fpython-cpython-execution-model-explained","\u002Fpython\u002Finternals\u002Fcpython-model",{"title":5,"description":380},"blog\u002Fpython-cpython-execution-model-explained","The CPython Execution Model","Memory & Internals","internals","2026-06-19","V00HIETRZg9SGA4FmNl-RwoepB1edb3B_J0RShEjYO4",1782244093422]