What does Python's `dis.dis()` print when you pass it a function?
answer
- A window on what the compiler emitted
- Reads the function's code object
- One row per instruction, with offsets
- Opargs resolved through the code object tables
- co_consts, co_varnames, co_names in parentheses
basics
~20 sdis.dis(f) prints the CPython bytecode the compiler produced for f: one line per instruction, with the source line, the byte offset, the opcode name and its resolved argument. It shows what the interpreter executes, not how long it takes.
solid answer
~50 s`dis.dis(f)` disassembles the code object hanging off `f.__code__` and prints its instruction stream. Each row carries the source line number, the byte offset, the opcode name, the raw argument, and in parentheses that argument resolved through the code object's side tables: `co_consts` for constants, `co_varnames` for locals, `co_names` for global and attribute names. It recurses into nested code objects such as inner functions, lambdas and generator expressions. Reach for it to answer *what does this line actually compile to* — why a name read from an enclosing module costs more than a local, or whether an arithmetic expression was folded to a single constant before the program ran. It explains costs; it does not measure them, so pair it with `timeit` or a profiler. The instruction set itself is an implementation detail that changes every minor release.
code
python · 7 linesimport dis
def average(prices):
return sum(prices) / len(prices)
dis.dis(average)
print(average.__code__.co_varnames, average.__code__.co_names, average.__code__.co_consts)go deeper
Be ready to say that Python compiles to bytecode first and that dis.dis prints those instructions for a function's code object. Knowing it exists and roughly what the columns mean is enough here.
Explain the columns and, crucially, which table each argument indexes: co_consts for constants, co_varnames for locals, co_names for globals and attributes. Show you would use get_instructions rather than parsing printed text.
Use disassembly to explain a cost you have already measured, never to guess at one. Expect to be asked why instruction counts mislead and why production behaviour must not depend on a specific opcode.
Own the policy that bytecode is a debugging aid, not a contract. Tooling that parses instruction streams pins the organisation to one interpreter version and one implementation, and that cost lands on every upgrade.
Python source is never executed as text. When CPython imports a module or evaluates a `def`, it parses the source, builds a syntax tree, optimises it and emits a **code object**: an immutable object holding a byte string of instructions plus the side tables those instructions index into. `dis` is the standard library's window on that object, and it is the only honest way to answer questions about what a line of Python "really does". ## What you are looking at A function carries its code object as `f.__code__`. `dis.dis(f)` finds it, walks the instructions and prints them in columns: ``` 4 LOAD_GLOBAL 1 (sum + NULL) LOAD_FAST_BORROW 0 (prices) CALL 1 BINARY_OP 11 (/) RETURN_VALUE ``` The leftmost column is the **source line number**, printed once per line of source. Then comes the **byte offset** into the instruction stream (shown only when you ask for it, or as a jump target label such as `L1:`), the **opcode name**, the raw integer **oparg**, and finally the oparg **resolved** in parentheses. That resolution is the part beginners miss: an oparg is just a small integer, and its meaning depends on the opcode. `LOAD_CONST 1` means "push item 1 of `co_consts`". `LOAD_FAST 0` means "push local slot 0", named by `co_varnames`. `LOAD_GLOBAL 1` and `LOAD_ATTR 0` index `co_names`, the table of names looked up dynamically. Print those three tuples next to the listing and the disassembly stops being noise: ```python print(f.__code__.co_consts, f.__code__.co_varnames, f.__code__.co_names) ``` Two things in the listing confuse newcomers. Labels such as `L1:` mark jump targets, and the jump instructions print `(to L1)` so you can follow a loop without doing offset arithmetic; `dis.dis(f, show_offsets=True)` prints the raw byte offsets alongside if you need them. Below the listing you may also see an `ExceptionTable`, which since 3.11 is how CPython records handler ranges instead of pushing block-setup instructions at run time — the reason a `try` that never raises now costs essentially nothing in the happy path. Neither is something to memorise; both are there so that what you read matches what the interpreter does. ## What else it accepts `dis.dis` takes far more than a function: a module, a class, a method, a raw code object, a traceback, or even a plain string of source, which it compiles first — `dis.dis("x = 1 + 2")` is a fine one-liner for settling an argument. It recurses into nested code objects (an inner function or a generator expression appears in the outer function's `co_consts`), and `depth=` bounds that recursion. `dis.distb()` disassembles the last traceback and marks the instruction that raised with an arrow, which is the fastest way to find *which* call on a dense line blew up. For tooling, do not scrape the printed text. `dis.get_instructions(f)` yields `Instruction` objects with `opname`, `argval`, `offset` and position information, and `dis.Bytecode(f)` wraps the same stream in an iterable object. `dis.code_info(f)` and `dis.show_code(f)` dump the code object's metadata — argument counts, flags, all the tables — without the instruction listing. ## What it is good for, and what it is not Good: explaining a cost. Why is a module-level loop slower than the same loop inside a function? The listing shows `LOAD_GLOBAL` where the function version shows a local read. Did the compiler fold `60 * 60 * 24`? `co_consts` answers in one line. Did that `if` disappear under `-O`? Compile with the flag and look. Not good: measuring. Instruction count is a weak proxy for time. A single `CALL` can run for a millisecond while five local loads cost nanoseconds, and since 3.11 the interpreter rewrites hot instructions at run time into specialised forms the plain listing does not show. The professional pattern is to measure first with `timeit` or a profiler, then disassemble to explain the difference you already observed. ## Bytecode is not a contract The `dis` module is stable API; the instruction set it prints is emphatically not. CPython documents bytecode as an implementation detail, and every minor release moves it. 3.11 introduced the specialising interpreter's opcode families, 3.12 merged `LOAD_METHOD` into `LOAD_ATTR` and added `RETURN_CONST`, and 3.14 added `LOAD_FAST_BORROW` and `LOAD_SMALL_INT` — and dropped `RETURN_CONST` again. A tool that parses `co_code`, asserts on exact opcode names, or ships pre-compiled bytecode pins you to one interpreter version and one implementation; that bill arrives at upgrade time. Read the listing, learn from it, and keep it out of your test assertions.
- How would you consume a disassembly from code rather than printing it?`dis.get_instructions(obj)` yields `Instruction` objects carrying `opname`, `argval`, `offset` and line information, and `dis.Bytecode(obj)` wraps the same stream in an iterable object. Both are what tooling should use; parsing the printed text is fragile because the formatting itself changes between releases.
- Can dis disassemble anything other than a function?Yes — a module, a class, a method, a code object, a traceback, or a plain string of source, which it compiles first. `dis.distb()` disassembles the last traceback and marks the instruction that raised, which pinpoints which call on a dense line failed.
- Why is instruction count a poor argument about speed?Opcodes differ enormously in cost: one `CALL` can dominate a hundred local loads, and since 3.11 the interpreter specialises hot instructions at run time into forms the static listing does not show. Measure with `timeit` or a profiler first, then use the disassembly to explain what you measured.
It is the assembly listing for your function: the compiler's own notes about what it decided to do, not a stopwatch on how fast it does it.
saying these in an interview costs you the question
- Treats dis output as a profiler with timings
- Assumes the printed bytecode is a stable, parseable API
- Thinks a .pyc file contains native machine code
- Confuses co_names with co_varnames
- Counts instructions and calls that a benchmark