posted: February 22, 2020
tl;dr: Life is easier if you can treat any object like a duck if it quacks like a duck...
The second in a series of posts highlighting the commonalities of Python and JavaScript.
As dynamically typed languages, both Python and JavaScript support duck typing. Duck typing is the ability to write code that can execute a named method on any object from any class that is passed to the code. As long as the named method exists inside the object, it will be invoked and the code should work. In simple terms, if the passed-in object has a quack() method like a duck does, then it will be treated like a duck.
As long as you don’t lock down your Python or JavaScript code by enforcing static type checking, duck typing can deliver tremendous benefits by helping to futureproof your code. Duck typing is my absolute favorite feature of Python, especially when combined with dunder methods (dunder is short for “double underscore”, which is their method naming convention), also known as magic methods. JavaScript’s duck typing implementation is a bit less powerful, but it’s still useful.
Here’s a simple Python example to illustrate the immense powers of duck typing:
def add_and_print(x, y):
result = x + y
print(x, 'added to', y, 'is', result)
This function doesn’t look like much, and it isn’t, but duck typing makes it work on a huge variety of inputs, now and in the future. My function accepts two arguments. Since Python is a dynamically-typed object-oriented language, these two arguments can be any two objects. Let’s try it on some different inputs, in the Python REPL:
>>> add_and_print(1, 2)
1 added to 2 is 3
>>> add_and_print(1.25, 2.25)
1.25 added to 2.25 is 3.5
>>> add_and_print('duck', 'typing')
duck added to typing is ducktyping
>>> add_and_print([1, 2], [3, 4])
[1, 2] added to [3, 4] is [1, 2, 3, 4]
>>> add_and_print((9, 8), (7, 6))
(9, 8) added to (7, 6) is (9, 8, 7, 6)
>>> add_and_print(True, False)
True added to False is 1
This seems pretty good, except perhaps for that last one. The first time the function was called, it was given two integers, and it added them together and printed out a result line. The second time it added two floating point numbers. The third time the arguments were two strings, and the strings were concatenated together by the addition (+) operator, which is a reasonable thing to do. The fourth time the function was given two lists (similar to JavaScript arrays), and it created a new list with all the elements of the first list followed by the second. It did the same thing in the fifth invocation when given two tuples (more precisely two 2-tuples, which is fun to say aloud).
However the sixth time the function was called, it was given two boolean values, and the result of ‘1’ is a little weird. What’s happening here is a type coercion: to turn the two boolean values into something that can be added, False is treated as 0 and True is treated as 1. The point I’m trying to illustrate is just that my function usually works, given a wide variety of input, without having to create explicit conditional branches depending on the types of the input.
What’s happening under the hood is duck typing combined with Python’s dunder methods (a.k.a. magic methods). You may think that the Python interpreter contains a bunch of code in it that, when it encounters the ‘+’ symbol, knows how to add together objects of various types on either side of the ‘+’. It doesn’t. All the interpreter is doing, when it encounters the ‘+’ symbol, is calling the __add__() method on the object to the left of the ‘+’ and passing it the object to the right. It is up to the class that the object was instantiated from to have an __add__() method. If it does, that method will be executed and a result will be returned from it.
Python’s built-in int (integer) type of course has an __add__() method. So does float, and str (string), and list, and tuple, etc. But here’s the genius part: thanks to duck typing, anyone who creates a new class, now or in the future, can write an __add__() method for it. The author gets to decide how two objects of that class should be added together. When my simple function is called by passing it two objects from that class, the __add__() method will be executed and my code should work. My very simple function is thereby futureproof (mostly - things can still go wrong, of course). There’s no need to upgrade the Python interpreter to be able to understand how to add together objects from this newly created class. There’s one more trick at work here: the print statement in my function relies upon the __str__() dunder method, which allows an author of a class to define how an object from that class should be printed.
Let’s say, years from now, someone comes up with a new way to store images in memory. Or waveforms. If they create a new class for it, and write an __add__()) and a __str__() method, my function will work when passed two objects from that class, thanks to duck typing. It’s up to the class author to decide how to add two images (or waveforms) together. Maybe the author wants to superimpose them, or maybe the author wants to concatenate one to the other. It is up to the author of the new class, as it should be. The Python interpreter doesn’t care. My function also doesn’t care: it is just going to implicitly call the two methods needed for it to work, __add__() and __str__().
Here’s the same example in JavaScript. There’s actually no duck typing happening, because JavaScript does not have built-in magic or dunder methods, which is one reason why duck typing is less commonly used in JavaScript than Python. I chose that Python example as the most elegant way that duck typing can be utilized. In JavaScript duck typing is accomplished by explicitly calling methods. Still, it is worthwhile taking a look at the JavaScript example, because it will show how duck typing can help simplify code.
Here’s my function in JavaScript:
function addAndPrint(x, y) {
const result = x + y;
console.log(x, 'added to', y, 'is', result);
}
And here are the results, from the NodeJS REPL, of calling that function with the same input as the Python example:
> addAndPrint(1, 2);
1 added to 2 is 3
undefined
> addAndPrint(1.25, 2.25);
1.25 added to 2.25 is 3.5
undefined
> addAndPrint('duck', 'typing');
duck added to typing is ducktyping
undefined
> addAndPrint([1, 2], [3, 4]);
[ 1, 2 ] added to [ 3, 4 ] is 1,23,4
undefined
> addAndPrint((9, 8), (7, 6));
8 added to 6 is 14
undefined
> addAndPrint(true, false);
true added to false is 1
undefined
The results from the first three function calls look identical to the Python example. The first two JavaScript function calls actually use the same JavaScript built-in type: JavaScript does not differentiate between integers and floating point numbers, as everything is just a number. The third JavaScript function call concatenates the two input strings, just like in the Python example, but not because of duck typing: that’s just what the addition operator does with strings in JavaScript.
The output of the fourth JavaScript function call, with two arrays (similar to Python lists) as inputs, looks very different. Because JavaScript isn’t doing duck typing in this example, it relies upon the built-in functionality of the addition operator to deal with the case of adding two arrays. The JavaScript runtime, as it often does, falls back to type coercion and converts each array to a string containing the values of the array, which it then concatenates.
There are two ways to get JavaScript to behave the same as Python in this case. One is to wait until the JavaScript runtime is upgraded so that the addition operator, when operating on two arrays, produces a result which is a new array containing all the values of the first array followed by the second array. That might be a nice JavaScript feature to have, but since there is so much JavaScript code in existence, some of which may rely upon the current behavior of the addition operator when given two arrays, that would be a breaking change to JavaScript. The other way is to add some code to my function to handle this case, as there is a concat() method for arrays. The function then becomes:
function addAndPrint(x, y) {
let result;
if (x instanceof Array) { result = x.concat(y); }
else { result = x + y; }
console.log(x, 'added to', y, 'is', result);
}
and it produces this result when passed two arrays, which is similar to the Python example:
> addAndPrint([1, 2], [3, 4]);
[ 1, 2 ] added to [ 3, 4 ] is [ 1, 2, 3, 4 ]
undefined
This shows why duck typing is powerful and helpful. Without it, my function needs to explicitly handle various types of input. With it, that responsibility is moved to the author of the class from which the objects are instantiated, which is a cleaner abstraction.
The second-to-last of the original JavaScript function calls isn’t at all equivalent to the Python example. In Python, (9, 8) is a 2-tuple literal, but in JavaScript, this same character sequence invokes the comma operator, which evaluates each element and produces a result which is the last element, or 8 in this case. As for the last function call with boolean values, the same thing is happening in both Python and JavaScript: both languages treat True/true as 1 and False/false as 0, in certain situations.
It is, of course, hard to write code which is 100% futureproof, but duck typing is a tremendous help. It also saves keystrokes and produces a cleaner abstraction when dealing with objects.