Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
365: Understanding Bytecode with dis
You've probably spent a lot of time now thinking about Python as a high-level language. We talk about objects, decorators, and generators. But at some point, you'll hit a wall where the code looks correct, the logic is sound, but the performance is just... off. Or maybe you're wondering why two different ways of writing the same logic result in different speeds. To answer those questions, you have to stop looking at the Python source code and start looking at the bytecode.
Let's look at a classic scenario. Imagine you're processing a massive stream of data and you need to filter it into a list. You might write something like this:
def build_list_loop(data):
result = []
for item in data:
if item > 10:
result.append(item)
return result
def build_list_comp(data):
return [item for item in data if item > 10]
Now, you know from experience that list comprehensions are generally faster. But why? If you're just guessing, you're not engineering; you're gambling. When I want to know the "why," I reach for the dis module.
The Overhead of Method Lookups
If we disassemble the build_list_loop function using dis.dis(), we can see exactly what the Python Virtual Machine (PVM) is doing. I'll focus on the inner loop part of the output:
import dis
def build_list_loop(data):
result = []
for item in data:
if item > 10:
result.append(item)
return result
dis.dis(build_list_loop)
You'll see a sequence like this in the output:
12 LOAD_FAST 1 (result)
14 LOAD_METHOD 0 (append)
16 LOAD_FAST 2 (item)
18 CALL_METHOD 1
Look closely at those instructions. Every single time the loop runs, Python has to LOAD_FAST the result list, then LOAD_METHOD to find where the append function lives on that list object, and then CALL_METHOD to actually execute it. That lookup—searching the object's dictionary for the "append" attribute—happens on every single iteration. It's a tiny tax, but over a million items, that tax becomes a massive bill.
The Efficiency of LIST_APPEND
Now, let's see what happens when we disassemble the list comprehension. This is where things get interesting.
dis.dis(build_list_comp)
In the bytecode for the comprehension, you won't see LOAD_METHOD or CALL_METHOD. Instead, you'll see something like this:
22 LOAD_FAST 1 (item)
24 LIST_APPEND 2
The LIST_APPEND instruction is a specialized opcode. It doesn't look up a method on an object; it tells the PVM to directly push the value onto the list currently being built. It bypasses the entire attribute lookup process. This is the "secret sauce" that makes comprehensions faster. It's not magic; it's just a more direct instruction to the virtual machine.
Using Bytecode to Debug Logic
I don't use dis every day, but it's an invaluable tool when I'm debugging subtle bugs related to closures or default arguments. For example, if you've ever wondered why def add_item(item=[]): causes the list to persist across function calls, looking at the bytecode reveals that the list is created once at definition time (as a constant attached to the function object) rather than at execution time.
When you're staring at a piece of code and the behavior doesn't match your mental model, stop trusting your intuition. Disassemble the function. The bytecode doesn't lie; it tells you exactly what the PVM is doing, regardless of how the sugar-coated Python syntax makes it look.
📋 Practical Task
Bytecode Comparison: Mapping vs. Loops
In this exercise, you will analyze the performance difference between a standard for loop and the map() function by examining their bytecode. Your goal is to identify the specific opcode that makes map() different from a manual loop.
Instructions:
- Create two functions:
square_loop(numbers)which uses aforloop to square numbers and append them to a list, andsquare_map(numbers)which usesmap(lambda x: x*x, numbers). - Use the
dismodule to disassemble both functions. - Print the disassembly of both to the console.
- Identify and write down (in a comment) which instruction is present in the
square_loopversion that is completely absent in thesquare_mapversion's primary execution path. - Observe how
maphandles the function call compared to how the loop handles the listappendmethod.
import dis
def square_loop(numbers):
res = []
for n in numbers:
res.append(n * n)
return res
def square_map(numbers):
return list(map(lambda x: x * x, numbers))
# Your task: Disassemble both and analyze the difference in how
# the 'append' operation is handled vs how map() handles the iteration.
dis.dis(square_loop)
print("-" * 30)
dis.dis(square_map)
There are no comments for now.