Python: A Strongly, Dynamically, Duck Typed, Interpreted Language
What is Python anyway?
Background and Motivation
I recently had a conversation with a few of my colleagues where I had mentioned that Python is a strongly typed language. One of them didn’t think that it was, and another explained that strong typing means that every object has a type, which is true about Python but is not the definition of strong typing. It surprised me to realize that even though my colleagues and I are experienced Python developers, none of us had a clear understanding of strong typing. This inspired me to write this post to help disambiguate Python and its runtime and static type systems.
Compiled vs. Interpreted Languages
A language itself is neither compiled nor interpreted. The terms “interpreted language” or “compiled language” signify that the canonical implementation of that language is an interpreter or a compiler, respectively. While interpretation and compilation are the two main means by which programming languages are implemented, they are not mutually exclusive, as most interpreting systems also perform some translation work, just like compilers. [1]
Compiled language implementations use a compiler to translate a program’s source code to the instructions of a target machine (machine code). For example, a +
operation in source code could be compiled directly to the machine code ADD
instruction. [2]
Interpreted language implementations use an interpreter to directly execute instructions without requiring them to have been compiled to machine code. An interpreter generally uses one of the following strategies for program execution:
- Parse the source code and perform its behavior directly. (Early versions of Lisp and BASIC)
- Translate source code into some efficient intermediate representation or object code and immediately execute that. (Python, Perl, MATLAB, and Ruby)
- Explicitly execute stored precompiled bytecode made by a compiler and matched with the interpreter Virtual Machine. (UCSD Pascal)
Some implementations may also combine two and three, such as contemporary versions of Java. [1]
Python has both compiled and interpreted implementations. CPython, Python’s reference implementation, can be defined as both an interpreter and a compiler, as it compiles Python code into bytecode before interpreting it. [3] PyPy, another Python implementation, is a just-in-time (JIT) compiler that compiles Python code into machine code at runtime.
Dynamic vs. Static Typing
In statically typed languages, variables have declared or inferred types, and a variable’s type cannot change. In dynamically typed languages like Python, values (runtime objects) have types, and variables are free to be reassigned to values of different types: [4]
|
|
Strong vs. Weak Typing
There is no precise technical definition of what the “strength” of a type system means, [5] but for dynamically typed languages, it is generally used to refer to how primitives and library functions respond to different types.
Python is considered strongly typed since most type changes require explicit conversions. A string containing only digits doesn’t magically become a number, as in JavaScript (a weakly typed language):
|
|
In Python, +
works on two numbers or two strings, but not a number and a string. This was a design choice made when +
was implemented for these classes and not a necessity following from Python’s semantics. Observe that even though strongly typed, Python is completely fine with adding objects of type int
and float
and returns an object of type float
(e.g., int(42) + float(1)
returns 43.0
), and when you implement +
for a custom type, you can implicitly convert anything to a number. [4]
Nominal vs. Structural Typing
Nominal typing is a static typing system that determines type compatibility and equivalence by explicit declarations and/or name matching. This means two variables are type-compatible if their declarations name the same type. [6] In general, Python’s static type system is nominally typed:
|
|
Abstract base classes (see abc
) allow you to define interfaces via inheritance, i.e. nominally. A class is abstract if it inherits from abc.ABC
or its metaclass is abc.ABCMeta
and it has at least one abstract method (a method decorated by abc.abstractmethod
). All abstract methods must be implemented by a subclass in order for that subclass to be concrete (instantiable).
abc.ABC.register
allows you to register an abstract base class as a “virtual superclass” of an arbitrary type. This allows virtual subclasses to pass runtime type checks like isinstance
and issubclass
, but has no effect on static type compatibility:
|
|
Structural typing is a static typing system that determines type compatibility and equivalence by the type’s actual structure or definition. [7] collections.abc.Iterable
is an example of a structural type in Python. It accepts any type that implements the __iter__
method.
|
|
collections.abc
provides a set of common abstract base classes that are useful for typing function parameters based on the operations performed on them, namely:
operation | magic method |
---|---|
... in x | __contains__ |
for ... in x | __iter__ |
next(x) | __next__ |
reversed(x) | __reversed__ |
len(x) | __len__ |
x(...) | __call__ |
x[...] | __getitem__ |
x[...] = ... | __setitem__ |
del x[...] | __delitem__ |
hash(x) | __hash__ |
Similar to abc.ABC
, subclasses of collections.abc
classes are nominally typed, which limits their usefulness for static typing.
typing.Protocol
provides a way to define custom structural types in Python. Any class that defines the same attributes and methods as a Protocol
is considered to be a subtype of that Protocol
:
|
|
Protocol
is a special type of ABC
. Its metaclass is _ProtocolMeta
, a subclass of ABCMeta
, and so it can be used as a drop-in replacement for ABC
with one caveat: Protocol
classes cannot inherit from non-Protocol
classes.
|
|
While Protocol
s do not require the use of inheritance, inheritance can still be used to provide default implementations of methods or default values for attributes:
|
|
Protocol
s are intended to define minimal interfaces for specific functionality without the use of inheritance. Consider using one when:
- Your interface is self-contained and is not implicitly required to be an instance of another type. Any functionality or structures that a
Protocol
depends on should be internally defined. - It is feasible to implement to your interface without using inheritance. You should avoid having methods with default implementations that would be impractical for someone to reimplement.
- Your interface is not likely to change, since they are not guaranteed to be automatically consumed via inheritance.
Conversely, consider using an ABC
if:
- Your interface is extending another class.
- Your interface is broad and not focused on a specific functionality.
- Your interface has complex default implementations.
- Your interface is likely to change.
typing.runtime_checkable
is a decorator for Protocol
classes that allows you to perform runtime type checks against them (with one exception):
|
|
isinstance
or issubclass
checks against runtime_checkable
Protocol
s only check for the presence of the required methods or attributes, not their type signatures or types.Duck Typing
Python’s runtime type system is duck typed. Duck typing is similar to structural typing but differs in that:
- Duck typing is a dynamic typing system.
- Compatibility is determined by checking only the parts of a type’s structure that are accessed at runtime.
It gets its name from the duck test: [8]
|
|
Protocol
s are a natural complement to duck typing since neither use inheritance to determine type compatibility. In our example above, we could define and use:
|
|
Recap
Is Python an interpreted language?
Python is considered an interpreted language because its reference implementation, CPython, compiles Python code into bytecode at runtime before interpreting it.
Is Python a dynamically typed language?
Python is a dynamically typed language because it allows variables to be reassigned to values of different types.
Is Python a strongly typed language?
Python is considered a strongly typed language because most type changes require explicit conversions.
Is Python’s static type system nominally or structurally typed?
Python’s static type system is generally nominally typed, but collections.abc
provides a collection of useful structural types, and typing.Protocol
provides a way to define custom structural types.
Is Python a duck typed language?
Python’s runtime type system is duck typed because it determines type compatibility by checking only the parts of an object’s structure that are accessed at runtime.
Sources
[1] Wikipedia: Interpreter (computing)
[2] StackOverflow: Compiled vs. Interpreted Languages - Answer by mikera
[4] StackOverflow: Is Python strongly typed? - Answer by community wiki
[5] Wikipedia: Strong and weak typing
[6] Wikipedia: Nominal type system