DEV Community

loading...

Dynamic nature of Python and the language design

hanpari profile image Pavel Morava ・7 min read

People tend to think that Python is dynamically typed language because you can do this:

x = 10
x = "Hello"

Well, you can do a similar assignment in Rust which is statically typed language.

let mut x = 10;
let x = "Hello";

Admittedly, I cheated here. This concept is called shadowing and Rust won't let you do this:

let mut x = 10;
x = "Hello";

On the other hand, shadowing is exactly what Python does when assigning a name to an object.

In Java or C# you can write something like:


 public class Example{

     public static void main(String []args){
        Object x = 10;
        System.out.println(x == 10); // error!


         x = "Hello";
        // This is fine as we cast Object to String.
        System.out.println(
            ((String)x).equals("Hello"));
     }
}

From the outer perspective, this resembles Python until you realize you don't need to perform any casting.

The difference appears as soon as we try to undertake a concrete action:

x = 10
assert x == 10
x = "Hello"
assert x == "Hello"

These lines in Python are perfectly fine.

Not so in Java as Java requires us to cast x to type, which supports concrete comparison before we perform it. Thus, we can continue assured everything is OK unless we encounter the feared NullReferenceException in runtime.

    public class Example{

        public static void main(String []args){
            String x = null;
            System.out.println(x.equals("Hello"));
            // NullReferenceException

        }
    }

Basically, Java needs to know the type in advance while Python doesn't care much. But because Python is strongly typed, you can rely on the interpreter to fail while Java happily compiles.

In Python

x = "Hello"
y = None

# x + y raises an exception.

While in Java:

public class Example{

    public static void main(String []args){
        String x = null;
        System.out.println( x + "hello");

    }
}

// Prints "nullhello"

Such behavior is a relict of the infamous Billion dollar mistake.

I call it my billion-dollar mistake. It was the invention of the null reference in 1965. At that time, I was designing the first comprehensive type system for references in an object oriented language (ALGOL W). My goal was to ensure that all use of references should be absolutely safe, with checking performed automatically by the compiler. But I couldn't resist the temptation to put in a null reference, simply because it was so easy to implement. This has led to innumerable errors, vulnerabilities, and system crashes, which have probably caused a billion dollars of pain and damage in the last forty years.

The modern implementations of Java or C# offer ways how to avoid it. This is why you should not neglect the design of the language. The persons who designed the first version of C#, not excluding evident and then-known flaws in Java, are now hailed for modern C#, which is admittedly quite good because, among other things, they somehow patched their initial mistakes. To be fair, Java hesitated much longer to improve despite a fair share of criticism.

For example, Rust doesn't suffer that as it is a fairly recent programming language. On the other hand, in Kotlin (another recent language), which deals with the old Java codebase and interoperability is a thing, you may run into problem as well, despite the fact that the pure Kotlin eradicates the issue by its design.

Let's finish with examples from two other languages:

Javascript

"hello" + undefined
// ends with "helloundefined"

PHP



 $x = 10;
 $y = null;
 $x + $y; // Is this obviously 10?
 $0 == null; // This is 1, meaning true

//What exactly is this expression? I have no idea
//echo failed to give me any information.
echo 0===null;

// At least, php7 gave me a warning when I tried this
"hello" + null;

But back to Python. Rightfully, you may ask why casting doesn't occur there.

In theory, you can guard your code in the following fashion:

if isinstance(y, int):
    y + y

In practice, it is more common to find:

try:
    y + y
except TypeError:
    pass #or whatever you'd like to do

Python's philosophy claims that it is easier to beg for forgiveness than to ask for permission.

What does it mean to you?

Consider the following:

from unittest import TestCase

test = TestCase()

class Animal:
    name = "Doggie"

class Person:
    name = "Hubert"

def get_name(x):
    return x.name

test.assertEqual(get_name(Animal()), "Doggie")
test.assertEqual(get_name(Person()), "Hubert")


Of course, the famous duck-typing in practice. In statically typed language, you would need to create and implement an interface to achieve a similar result. (See I did not use the Inheritance word here because in OOP domain they do prefer composition over inheritance. If you need a good example, I suggest studying how they have implemented composition in Kotlin, for instance here.) Rust lacks inheritance completely, implementing [traits](https://en.wikipedia.org/wiki/Trait_(computer_programming) instead.

No doubt, interfaces convey your intention more safely and predictably, but frankly, bother with them each time is a hassle.

Now, you may be curious about what happens when get_name receives type not containing field name. Let's check it up.

class Anything:
    pass

with test.assertRaises(AttributeError) as _:
    get_name(Anything())

try:
    test.assertIsNotNone(Anything().name)
except AttributeError:
    print("The name is not None for sure!")
The name is not None for sure!

Basically, we got the exception and confirmed by the experiment that the field name is not None. Why did I do this? To demonstrate the safety Python provides when compared to another mainstream dynamic typed language, Javascript.

let anything = {};
let get_name = (x)=>x.name;
get_name(anything.name)
//undefined

This is an important difference in the design of these two languages. If you neglect to inspect your object for field name, Python fails if this one is not present while Javascript just continues to run - which behavior together with its weak-typing nature and weird coercion rules may lead to absolutely unpredictable results and hard-to-find bugs.

Now, let's go back to a dynamic nature of Python. In the declaration of class Anything we omitted the field name. But we can do this.

car = Anything()
car.name = "Škoda"
test.assertEqual(get_name(car), "Škoda")

Phew, now it is getting complicated. By default, we can add a field in runtime as we desire. Do you remember me mentioning interfaces? How would static-typed language manage to implement following god-like class that tries to deal with the variety of the real world?

from typing import Optional
class Car:
    def __init__(self, **kwargs):
        self.__dict__ = kwargs

    def is_truck(self)->bool:
        return hasattr(self, "maximum_load")

    def get_maximal_number_of_persons(self)->Optional[int]:
        try:
            return self.persons
        except AttributeError:
            return None

    def get_name_of_all_fields(self):
        return list(self.__dict__.keys())

    def __str__(self):
        return f"Car: {self.name if hasattr(self, 'name') else 'Uknown'}"

skoda = {
    "name": "Škoda",
    "persons": 5,
    "type": "Fabia",
}

tatra = {
    "name": "Tatra",
    "maximum_load": 1000,
    "fuel": "diesel",
}

skoda_car = Car(**skoda)
tatra_truck = Car(**tatra)

test.assertEqual(get_name(skoda_car), "Škoda")
test.assertFalse(skoda_car.is_truck())
test.assertEqual(skoda_car.type, "Fabia")
test.assertIsNotNone(skoda_car.persons)
test.assertTrue(hasattr(tatra_truck, "fuel"))

The programmers in statically typed languages scream in pain. What a horror! It is absolutely unpredictable! This is what the word dynamic means, a constant change.

In fact, thanfully, not all code in Python leverages its dynamicity, so you don't need to worry so much. To bring more order in dynamic chaos, Python in the third version embraces optional static typing which gets better with every iteration.

But back to our dynamic business.
Sooner or later, every aspiring Pythonista encounters all-mightly construction in a method or function signature which reads: (*args,**kwargs)

For instance:

def kill_them_all(*args, **kwargs):
    return f"I killed {args} and also {kwargs}"

kill_them_all("dogs", "people", "bacteria", numbers=(1,2,3.4), function=print, any_object=object())
"I killed ('dogs', 'people', 'bacteria') and also {'numbers': (1, 2, 3.4), 'function': <built-in function print>, 'any_object': <object object at 0x00000264B226CD00>}"

This voracious function knows no mercy. It accepts everything, with no regards to numbers, types or purpose; in a way, you have just shaken your hand with the ultimate predator of Pythonic jungle.

Why would the benevolent dictator create such a horrendous creature, swallowing chunks of code indiscriminately?

Why indeed? You can inspect args and kwargs, or even better you can push them to another hungry throat. Behold!

def print_only_cars(*args):
    if all(isinstance(obj, Car) for obj in args):
        print(*args)
    else:
        print("I refuse to print:", *args)

print_only_cars(1,2,3)
print(skoda_car, tatra_truck, Car())

I refuse to print: 1 2 3
Car: Škoda Car: Tatra Car: Uknown




Conclusion

Hopefully, I have demonstrated a few aspects of the dynamic behavior of Python. Whatever may proponents of static typing claim, the dynamic typing is not inferior, in fact, the statical typing is just a subset of elastic nature of dynamic typing.

Admittadly, one pays for that power, namely by lesser readability, the slower speed of execution, and harder refactoring. On the other hand, to restrict ourselves to the statically-typed paradigm would be foolish. Take, for instance, a great modern language like Rust. To achieve similar results as shown here, if even possible that easy, would require to dive into the arcane syntax of Rust's macros and in there, all readability goes to hell.

Moreover, being superset of static typing, dynamically typed languages, namely Python with type annotations or Typescript for Javascript, draw benefits from both worlds.

The language design matters. Some languages, even if you do not agree with the way they are implemented, adhere to inner consistency while other are just set of random rules; rules which their creator found convenient in the time he/she was creating language and which, in the end, makes such languages highly dangerous places to live and code.

There is no perfect language out there. All are flawed or suboptimal if you prefer. Still, some are safer than the others. If you can, choose the better ones.

Discussion

pic
Editor guide