Skip to content

Understanding Python Bytecode vs Native Code

Learn how Python source code is compiled into bytecode, how the Python Virtual Machine (PVM) executes it, how native machine code differs, and why libraries written in C, C++, and Rust can significantly improve performance and enable efficient multithreading.


1. What Happens When You Run a Python Program?

When you run a Python program, your source code is not executed directly by the computer's processor. Instead, Python first converts your code into an intermediate form called bytecode, which is then executed by the Python Virtual Machine (PVM).

The overall execution process can be summarized as follows:

+------------------+
| Python Source    |
|    (.py file)    |
+------------------+
          |
          | Compile
          v
+------------------+
|    Bytecode      |
|   (.pyc file)    |
+------------------+
          |
          | Execute
          v
+--------------------------+
| Python Virtual Machine   |
|         (PVM)            |
+--------------------------+
          |
          | CPU Instructions
          v
+------------------+
| Computer Hardware|
+------------------+

The process consists of three main steps:

  1. Compilation – Python compiles the source code into bytecode.
  2. Execution – The Python Virtual Machine (PVM) reads and executes the bytecode instructions.
  3. Hardware Execution – The operating system and processor execute the machine instructions required by the PVM.

Unlike languages such as C or C++, Python does not compile your program directly into native machine code that the processor can execute. Instead, Python relies on the Python Virtual Machine (PVM) to interpret the generated bytecode.

If you have a Java background, this execution model may already be familiar. Like Python, Java first compiles source code into bytecode, which is then executed by the Java Virtual Machine (JVM). Although the Python Virtual Machine and the Java Virtual Machine are different implementations with different capabilities, both use bytecode as an intermediate representation rather than executing source code directly.

This extra execution layer makes Python highly portable. The same Python source code can run on Windows, Linux, and macOS without modification, provided a compatible Python interpreter is installed.

In the following sections, you'll learn what bytecode is, how it differs from native machine code, and why many high-performance Python libraries use native code internally.


2. What Is Bytecode?

Bytecode is an intermediate, platform-independent representation of your Python program. It is generated automatically by the Python interpreter after your source code is parsed and compiled.

Unlike your Python source code, bytecode is not human-readable, and unlike native machine code, it cannot be executed directly by the processor. Instead, it is designed to be executed by the Python Virtual Machine (PVM).

For example, consider the following Python program:

x = 10
y = 20

print(x + y)

Before execution, Python compiles the program into a sequence of bytecode instructions.

Conceptually, the process looks like this:

Python Source Code
        |
        | Compile
        v
+----------------------+
| LOAD_CONST 10        |
| STORE_NAME x         |
| LOAD_CONST 20        |
| STORE_NAME y         |
| LOAD_NAME x          |
| LOAD_NAME y          |
| BINARY_ADD           |
| PRINT                |
+----------------------+

These instructions are not CPU instructions such as those executed by Intel or ARM processors. Instead, they are instructions that the Python Virtual Machine understands.

The PVM reads each bytecode instruction one at a time and performs the corresponding operation. For example:

  • LOAD_CONST loads a constant value.
  • STORE_NAME stores a reference in a variable.
  • LOAD_NAME retrieves the value of a variable.
  • BINARY_ADD adds two values.
  • PRINT displays the result.

Python may also store the compiled bytecode in a .pyc file. If the source file has not changed, Python can reuse the cached bytecode the next time the program runs, reducing startup time by avoiding recompilation.

Because bytecode is platform-independent, the same Python program can run on different operating systems without being recompiled. Each platform simply uses its own Python interpreter to execute the bytecode.


3. What Is Native Machine Code?

Unlike bytecode, native machine code consists of instructions that can be executed directly by the computer's processor without requiring a virtual machine.

Every processor architecture (such as Intel x86-64 or ARM) has its own instruction set. When a program is compiled into native machine code, it is translated into instructions that are specific to the target processor.

For example, consider the following C program:

#include <stdio.h>

int main() {
    int x = 10;
    int y = 20;

    printf("%d\n", x + y);

    return 0;
}

A C compiler (such as GCC or Clang) translates this program directly into native machine code.

Conceptually, the compilation process looks like this:

+------------------+
|   C Source Code  |
|     (.c file)    |
+------------------+
          |
          | Compile
          v
+------------------+
| Native Machine   |
|      Code        |
+------------------+
          |
          | Execute
          v
+------------------+
| Computer Hardware|
+------------------+

Unlike Python bytecode, native machine code is executed directly by the CPU. There is no Python Virtual Machine involved.

Because the instructions are specific to a processor architecture, a program compiled for one platform cannot normally run on another without recompilation.

For example:

  • A Windows executable (.exe) cannot run directly on Linux.
  • A program compiled for an Intel processor cannot run directly on an ARM processor.
  • Separate binaries are often required for Windows, Linux, and macOS.

The primary advantage of native machine code is performance. Since the processor executes the instructions directly, native programs typically run much faster than programs whose bytecode must first be interpreted by a virtual machine.

Many high-performance Python libraries, such as NumPy, OpenCV, and PyTorch, perform their computationally intensive work using native machine code written in languages like C or C++. Python acts as a convenient interface, while the heavy computations are delegated to highly optimized native code.


4. Why Does Python Use Bytecode?

If native machine code executes faster than bytecode, you might wonder why Python uses bytecode at all.

The answer is that bytecode provides several important advantages.

Platform Independence

Bytecode is not tied to a specific processor or operating system. The same Python program can run on Windows, Linux, and macOS without being recompiled.

          Python Source
                |
                v
           Python Bytecode
          /       |       \
         /        |        \
   Windows     Linux     macOS
      |           |          |
      v           v          v
     PVM         PVM        PVM

Each platform simply uses its own Python interpreter to execute the bytecode.


Faster Program Startup

Python can save compiled bytecode in .pyc files. When the source code has not changed, Python can reuse the cached bytecode instead of compiling the source again.

This reduces the startup time of Python programs.


Simplified Interpreter Design

The Python interpreter only needs to understand one instruction set, the Python bytecode instruction set.

Regardless of the operating system or processor, the interpreter executes the same bytecode instructions.


Easier Maintenance

Because bytecode is platform-independent, developers write a program only once.

There is no need to compile separate versions for Windows, Linux, macOS, Intel processors, and ARM processors. As long as a compatible Python interpreter is available, the same program can be executed.

Although bytecode introduces some execution overhead compared to native machine code, the benefits of portability, simplicity, and ease of development make it an excellent choice for a general-purpose programming language like Python.


4. Is Python Bytecode Part of the Python Standard?

A common misconception is that Python has a standard bytecode format similar to Java. In reality, this is not the case.

The Python language specification defines the syntax and behavior of the language, but it does not define a standard bytecode instruction set.

Instead, each Python implementation is free to choose how it compiles and executes Python programs, provided it conforms to the Python language specification.

For example:

  • CPython compiles Python source code into its own bytecode (.pyc files), which is executed by the Python Virtual Machine (PVM).
  • PyPy uses its own internal bytecode and a Just-In-Time (JIT) compiler to improve performance.
  • Jython compiles Python code into Java bytecode, which is executed by the Java Virtual Machine (JVM).
  • IronPython compiles Python code into .NET Intermediate Language (CIL/MSIL), which is executed by the .NET Common Language Runtime (CLR).

Conceptually, the various implementations look like this:

               Python Source Code
                      |
      +---------------+---------------+
      |               |               |
      v               v               v
   CPython         Jython        IronPython
      |               |               |
      v               v               v
CPython Bytecode  Java Bytecode   .NET IL (CIL)
      |               |               |
      v               v               v
     PVM             JVM             CLR

Although these implementations use different execution models internally, they all aim to behave the same from a programmer's perspective. This allows the same Python source code to run across different implementations with little or no modification.

This article focuses on CPython, which is the reference implementation of Python and the version used by the vast majority of Python developers.


5. How Python Uses Native Machine Code

Although Python programs are executed as bytecode by the Python Virtual Machine (PVM), Python can also execute native machine code through extension modules.

Many popular Python libraries are written entirely or partially in languages such as C, C++, or Rust. These languages compile directly into native machine code, allowing computationally intensive tasks to execute much faster than equivalent Python code.

Conceptually, the execution flow looks like this:

          Python Program
                 |
                 v
         Python Bytecode
                 |
                 v
     Python Virtual Machine
                 |
      +----------+-----------+
      |                      |
      | Python Code          | Calls Native Library
      |                      |
      v                      v
 Python Bytecode      Native Machine Code
      |                      |
      +----------+-----------+
                 |
                 v
          Computer Hardware

For example, consider a NumPy program:

import numpy as np

a = np.arange(1_000_000)
b = a * 2

Although this code appears to be ordinary Python, the multiplication is not performed by Python bytecode.

Instead:

  1. Python executes the statement b = a * 2.
  2. The * operation invokes NumPy.
  3. NumPy performs the multiplication using highly optimized native machine code.
  4. The result is returned to the Python program.

The same approach is used by many popular libraries.

Library Native Language(s)
NumPy C
Pandas C, Cython
OpenCV C++
PyTorch C++
TensorFlow C++
SciPy C, C++, Fortran

This hybrid approach combines the best of both worlds:

  • Python provides a simple, expressive language for writing applications.
  • Native code performs the computationally intensive work with high performance.

As a result, Python developers can write concise, readable programs while still achieving performance close to that of compiled languages for many tasks.


6. Why Is Native Machine Code Faster?

Native machine code is generally much faster than Python bytecode because it is executed directly by the processor. In contrast, every Python bytecode instruction must first be interpreted by the Python Virtual Machine (PVM).

Conceptually, the execution paths look like this:

Python Bytecode

Bytecode Instruction
        |
        v
Python Virtual Machine
        |
        v
CPU


Native Machine Code

Machine Instruction
        |
        v
CPU

Eliminating the extra interpretation step significantly reduces execution overhead.

In addition, native code benefits from several compiler optimizations that are not available to Python bytecode.

Compiler Optimizations

Compilers such as GCC, Clang, and MSVC analyze the program and generate highly optimized machine instructions.

These optimizations include:

  • Eliminating unnecessary instructions.
  • Reordering instructions for better CPU utilization.
  • Vectorizing operations using SIMD instructions.
  • Optimizing memory access and CPU cache usage.

These optimizations allow native programs to execute much more efficiently than interpreted bytecode.

True Parallel Execution

Python bytecode executed by the Python Virtual Machine is subject to the Global Interpreter Lock (GIL) in CPython. As a result, only one thread executes Python bytecode at a time within a single process.

Many native libraries overcome this limitation by performing their computationally intensive work outside the Python interpreter. During these operations, they can release the GIL and execute code using multiple CPU cores simultaneously.

For example, libraries such as NumPy, OpenCV, and PyTorch can perform matrix operations, image processing, and deep learning computations using multiple threads, providing substantial performance improvements on multicore systems.

Optimized Algorithms

High-performance libraries are often implemented using algorithms and data structures specifically designed for numerical computing, image processing, and scientific applications.

As a result, a single function call in Python may trigger thousands or even millions of highly optimized native machine instructions behind the scenes.

This is why Python applications can achieve excellent performance despite being an interpreted language. The performance-critical parts of the program are often executed as optimized native machine code rather than Python bytecode.


7. When Should We Use Native Code?

For most applications, pure Python is sufficient. Python's readability, simplicity, and extensive standard library make it an excellent choice for developing applications quickly.

However, some tasks involve millions or even billions of computations. In such cases, the overhead of interpreting Python bytecode can become significant.

These performance-critical tasks are good candidates for native machine code.

Typical Use Cases

The following table lists common scenarios where native libraries are preferred.

Application Common Native Libraries
Numerical computing NumPy, SciPy
Data analysis Pandas
Image processing OpenCV
Machine learning TensorFlow, PyTorch
Scientific computing SciPy
Cryptography OpenSSL, cryptography
Compression zlib, LZMA
Database engines SQLite

These libraries expose a simple Python interface while performing the computationally intensive work using optimized native machine code.

For example, multiplying two large matrices may require billions of arithmetic operations. Writing such an algorithm in pure Python would be much slower than using NumPy, which delegates the computation to highly optimized native code.

import numpy as np

a = np.random.rand(1000, 1000)
b = np.random.rand(1000, 1000)

c = a @ b

Although this looks like ordinary Python code, the matrix multiplication is performed by optimized native libraries such as BLAS or LAPACK behind the scenes.

Do We Need to Write Native Code?

In most cases, no.

A common development approach is:

  1. Write the application in pure Python.
  2. Measure its performance using profiling tools.
  3. Identify performance bottlenecks.
  4. Replace only the computationally intensive parts with optimized libraries or native extensions if necessary.

This approach combines Python's ease of development with the performance of native machine code, allowing developers to build efficient applications without sacrificing readability or maintainability.


8. A Note About the Global Interpreter Lock (GIL)

In CPython, Python bytecode is executed under the control of the Global Interpreter Lock (GIL). The GIL ensures that only one thread executes Python bytecode at a time within a single process.

This might suggest that Python programs cannot take advantage of multiple CPU cores. However, many native libraries such as NumPy, OpenCV, and PyTorch perform their computationally intensive work outside the Python interpreter. During these operations, they can release the GIL, allowing multiple native threads to execute simultaneously on different CPU cores.

As a result, although Python bytecode itself is limited by the GIL, native extension modules can often achieve true parallel execution for CPU-intensive tasks.

The Global Interpreter Lock is an important topic in its own right and is discussed in detail in the article Understanding the Global Interpreter Lock (GIL) in Python.


9. Summary

  • Python source code is first compiled into bytecode, an intermediate representation that is executed by the Python Virtual Machine (PVM).
  • Unlike C and C++, Python does not compile programs directly into native machine code.
  • Bytecode is platform-independent, allowing the same Python program to run on different operating systems without recompilation.
  • CPython uses its own bytecode format, while other Python implementations such as Jython and IronPython use different execution models internally.
  • Native machine code consists of processor-specific instructions that are executed directly by the CPU, providing significantly better performance.
  • Many popular Python libraries, including NumPy, Pandas, OpenCV, PyTorch, and TensorFlow, perform computationally intensive work using optimized native code.
  • Native code is faster because it executes directly on the processor, benefits from compiler optimizations, and can often utilize multiple CPU cores efficiently.
  • In most applications, you should write your program in pure Python and rely on optimized native libraries for performance-critical operations rather than writing native code yourself.

Python Material on this website