Is Python a Statically or Dynamically Typed Language?

Understanding dynamic typing in Python through bytecode and runtime type checks
Python
type checking
runtime
Author
Published

August 23, 2026

Today, I am going to talk about intrinsic characteristics of Python.

Have you ever received a TypeError while running your Python program?

It occurs because Python is a dynamic type language. It means the language verifies variable types during execution (runtime) - not during compilation, even though Python does compile your code into bytecode before running it. The type check only happens the instant an operation is actually executed, not when the code is first parsed and compiled.

Let’s go with an example:

def python_dynamic_language_example(execute_invalid_operation):
    print("The function was compiled to bytecode already")

    if execute_invalid_operation:
        a = 5
        b = "5"
        return a + b
    return "No invalid operation was found"

This function can run successfully even though there is an invalid operation inside the if clause:

python_dynamic_language_example(False)
The function was compiled to bytecode already
'No invalid operation was found'

Moreover, since this is a Quarto document, the mere fact that the whole .qmd file compiled successfully already shows the function was compiled to bytecode without error.

However, when we try to access the code portion that does the calculation, we receive the TypeError:

python_dynamic_language_example(True)
The function was compiled to bytecode already
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[3], line 1
----> 1 python_dynamic_language_example(True)

Cell In[1], line 7, in python_dynamic_language_example(execute_invalid_operation)
      3 
      4     if execute_invalid_operation:
      5         a = 5
      6         b = "5"
----> 7         return a + b
      8     return "No invalid operation was found"

TypeError: unsupported operand type(s) for +: 'int' and 'str'

which means that Python did not perform the operation because it was operated between two invalid types, even though it was compiled, as can be seen by dis:

import dis
dis.dis(python_dynamic_language_example)
  1           0 RESUME                   0

  2           2 LOAD_GLOBAL              1 (NULL + print)
             12 LOAD_CONST               1 ('The function was compiled to bytecode already')
             14 CALL                     1
             22 POP_TOP

  4          24 LOAD_FAST                0 (execute_invalid_operation)
             26 POP_JUMP_IF_FALSE        9 (to 46)

  5          28 LOAD_CONST               2 (5)
             30 STORE_FAST               1 (a)

  6          32 LOAD_CONST               3 ('5')
             34 STORE_FAST               2 (b)

  7          36 LOAD_FAST                1 (a)
             38 LOAD_FAST                2 (b)
             40 BINARY_OP                0 (+)
             44 RETURN_VALUE

  8     >>   46 RETURN_CONST             4 ('No invalid operation was found')

The fact that Python compiled this code, it means that it is dynamically type, instead of statically typed:

Languages that verify the type of the variable before runtime, such as Javaand C++, are known as static type language. The advantage of it is that in production you have more guarantees that your code will not break due to an accidental variable type change or operation. So, you know in advance that a mistake can occur.

The fact that Python did not allow that operation occur in runtime means that it is strongly typed, instead of weakly typed:

Weak typed languages, such as JavaScript and PHP would allow this operation to occur. It would result in "55" (string) in JavaScript and 10 (integer) in PHP.

Having these concepts in mind can help you to think on which language to use, or anticipate errors.

Flexibility in Python

Even though this is an odd behavior in Python, it allows you the change the variable type while programming:

a = 5
print(type(a))
print(id(a))
a = "5"
print(type(a))
print(id(a))
<class 'int'>
103300731305384
<class 'str'>
103300731366408

It occurs because the value has a type (dynamic type language), not the variable. It also allows you to write less boilerplate code, and make your code into production faster.

What if you want to put more restrictions in Python ?

In recent Python versions (3.x), what you can do is add type annotation to variables and output of functions. However, it will still allow the function to be compiled:

def python_dynamic_language_example_typed(
    execute_invalid_operation: bool,
) -> str | TypeError:
    print("The function was compiled to bytecode already")

    if execute_invalid_operation:
        a: int = 5
        b: str = "5"
        return a + b
    return "No invalid"


dis.dis(python_dynamic_language_example_typed)
  1           0 RESUME                   0

  4           2 LOAD_GLOBAL              1 (NULL + print)
             12 LOAD_CONST               1 ('The function was compiled to bytecode already')
             14 CALL                     1
             22 POP_TOP

  6          24 LOAD_FAST                0 (execute_invalid_operation)
             26 POP_JUMP_IF_FALSE        9 (to 46)

  7          28 LOAD_CONST               2 (5)
             30 STORE_FAST               1 (a)

  8          32 LOAD_CONST               3 ('5')
             34 STORE_FAST               2 (b)

  9          36 LOAD_FAST                1 (a)
             38 LOAD_FAST                2 (b)
             40 BINARY_OP                0 (+)
             44 RETURN_VALUE

 10     >>   46 RETURN_CONST             4 ('No invalid')

However, if you are coding in an editor and uses a tool that does live checking, such as Pylance in Vs Code in standard mode, it would warn you about the operation:

and you could fix it right away.

These are concepts that you may not even realize that are important unless you know it.

Happy coding!

Back to top