keepcalculator

06 Numbers and Powers

Python Thinks Differently: Inside Your Code

Card No. 06·4182 · Updated · Standard formula, shown below

Average pace 5.00 min/km
Average speed
12.00 km/h
Result
Finish time
50 min
You entered
Distance
10 km
You entered
You enter
Finish time, Distance
You get
Average pace, Average speed
Rounding
2 dp

When you run a Python program, the interpreter does not execute your code line by line in the way you might imagine. Instead, Python first compiles your source code into an intermediate set of instructions called bytecode, and then a virtual machine executes that bytecode, which is why Python behaves differently from compiled languages like C or Java. This article explains what actually happens inside your code from the moment you press Run, covering the interpreter, bytecode, memory management, and common misconceptions.

Formula

The calculator works out your average pace and speed for a run or ride. You enter the finish time in minutes and the distance in kilometers. The first output, average pace, is the time per kilometer, calculated as:

pace (min/km) = finish time (min) / distance (km)

The second output, average speed, is the distance divided by the time in hours, calculated as:

speed (km/h) = distance (km) / (finish time (min) / 60)

Both outputs use the same two inputs, so they are directly related: a faster pace means a higher speed.

Worked Example

Using the default values from the calculator block: finish time = 50 minutes, distance = 10 km.

  1. Average pace = 50 / 10 = 5.00 min/km
  2. Average speed = 10 / (50 / 60) = 10 / 0.8333 = 12.00 km/h

So for a 50-minute 10 km run, your average pace is 5 minutes per kilometer and your average speed is 12 km/h. The calculator rounds to two decimal places.

Why Python Uses an Interpreter

Python is often called an interpreted language, but that is only half the story. Unlike C or C++, which are compiled entirely into machine code before execution, Python compiles your source code into bytecode on the fly. This bytecode is a low-level, platform-independent representation of your program that the Python virtual machine (PVM) then executes. This two-step process is what makes Python portable across operating systems without recompilation, but it also adds a layer of overhead that makes Python slower than fully compiled languages.

The interpreter reads your source file, parses it into an abstract syntax tree (AST), and then compiles that AST into bytecode. The bytecode is cached in .pyc files (inside the __pycache__ folder) so that subsequent runs skip the compilation step if the source hasn’t changed. This is why the first run of a script can feel slower than later runs.

What Is Bytecode Anyway?

Bytecode is a set of instructions that the Python virtual machine understands. Each instruction is a simple operation like “load a variable�, “add two numbers�, or “call a function�. For example, the code a = 1 + 2 is compiled into a sequence of bytecode instructions that load the constants 1 and 2, add them, and store the result in the variable a. You can see the bytecode for any function using the dis module:

import dis
def add(a, b):
    return a + b
dis.dis(add)

This prints a table of bytecode instructions, each with a line number, an operation name, and an argument. The virtual machine executes these instructions one by one, using a stack to hold intermediate values. This stack-based design is similar to how Java’s JVM works, but Python’s bytecode is higher-level and more dynamic.

How Python Manages Memory

Python uses automatic memory management, meaning you don’t have to manually allocate or free memory. The two main mechanisms are reference counting and a garbage collector. Every object in Python has a reference count, which is the number of names or data structures that point to it. When the count drops to zero, the object is immediately deallocated. This is why Python can be memory-hungry: even temporary objects are created and destroyed constantly.

The garbage collector handles circular references, where two objects point to each other but are no longer reachable from the rest of the program. Without a collector, their reference counts would never reach zero. The gc module lets you inspect and tune the collector, but most programs never need to touch it.

Python also uses a private heap to store all objects. The heap is managed by the memory manager, which includes an allocator that handles small objects (under 512 bytes) in a special way to reduce fragmentation. This is why Python can use more memory than a C program doing the same task—every integer, string, or list is a full object with type information and reference count overhead.

Dynamic Typing and Runtime Behavior

One of the biggest differences between Python and compiled languages is dynamic typing. In Python, variables are just names that refer to objects; they don’t have a fixed type. The type is checked at runtime, when an operation is performed. For example, a = 1 then a = "hello" is perfectly valid. This flexibility makes Python easy to write, but it also means the interpreter cannot optimize code based on type information, which contributes to slower execution.

At runtime, Python looks up attributes and methods dynamically. When you call obj.method(), the interpreter searches the object’s class and its parents for a method with that name. This lookup happens every time the call is made, unless you cache the method in a local variable. This is why Python code can be slower than C, where function calls are resolved at compile time.

Common Misconceptions About Python Execution

A common myth is that Python compiles to machine code like C. It does not—it compiles to bytecode that runs on the virtual machine. Another misconception is that Python is “interpreted� in the sense that each line is executed without any compilation. In reality, the whole file is compiled to bytecode before execution begins, which is why syntax errors are caught before any line runs. However, some errors like division by zero only appear when that line executes.

Another misconception is that the Global Interpreter Lock (GIL) makes Python single-threaded. The GIL is a mutex that allows only one thread to execute Python bytecode at a time, which simplifies memory management. But I/O-bound programs can still benefit from threading, because the GIL is released during I/O operations. CPU-bound programs, on the other hand, may not see a speedup with threads; using multiple processes or libraries like NumPy that release the GIL is often better.

How to Choose Between Python and Other Languages

Understanding how Python executes helps you decide when to use it. Python is ideal for scripting, data analysis, web backends, and automation, where development speed matters more than raw performance. For CPU-intensive tasks like video processing or high-frequency trading, compiled languages like C or Rust are better because they produce native machine code. However, you can often combine both: write the performance-critical parts in C and call them from Python using tools like Cython or ctypes.

If you are building a large application, consider that Python’s dynamic nature makes it easier to write but harder to maintain as the codebase grows. Static type hints (PEP 484) help, but they are optional and not enforced at runtime. For projects where type safety is critical, languages like Go or Java might be a better fit.

Frequently Asked Questions

Why is Python slower than C?

Python is slower because it executes bytecode on a virtual machine rather than native machine code, and because dynamic typing forces runtime type checks and attribute lookups. Each operation carries overhead that C avoids by compiling directly to optimized machine code.

What does the .pyc file do?

A .pyc file contains the compiled bytecode of a module. It is stored in the __pycache__ directory and is used to skip the compilation step on subsequent runs, making program startup faster. The bytecode is version-specific, so different Python versions have different .pyc files.

Is Python compiled or interpreted?

Python is both. It is compiled to bytecode, and then the bytecode is interpreted by the virtual machine. This is similar to Java, but Python’s bytecode is higher-level and the compilation happens automatically at runtime.

How does the GIL affect my multithreaded program?

The GIL allows only one thread to execute Python bytecode at a time, so CPU-bound threads don’t run in parallel. For I/O-bound tasks, the GIL is released during I/O, so threads can overlap. To get true parallelism, use the multiprocessing module or write the hot code in C.

Can I see the bytecode of my code?

Yes, use the dis module. Import dis and call dis.dis(your_function) to print the bytecode instructions. This is useful for understanding how Python executes your code and for optimizing performance.

Related

Formula used
in t | Finish time | min | 50 | 0
in d | Distance | km | 10 | 0 | 100000
out pace | Average pace | min/km | t / d | 2
out speed | Average speed | km/h | d / (t / 60) | 2

Each line is one input (in, sel, list) or one result (out: label, unit, formula, decimals). How the calculators work

Method: the standard formula for this calculation, rounded as each result states. See how the calculators work or report an error.

More cards in this drawer

The whole Numbers and Powers drawer →