Weird Python Stuff You Might Not Have Seen Before | by Emmett Boudreau | Towards Data Science

The Python programming language is a programming language that is filled to the absolute brim with amazing multi-paradigm features. This is great because it gives Python the flexibility to do a lot of different tasks. For example, if Python was a purely object-oriented programming language with no support for generic programming concepts, it likely would not be quite as popular in the world of Data Science. This is because Data Scientists typically desire a high-level declarative programming language where things can be defined in a global scope. Just imagine writing a main function for a Jupyter-Notebook. Now that would be strange.
Naturally, with all of these interesting explorations of paradigm and syntax, the world of Python can actually be quite vast and typically some things can easily be overlooked because there is simply a lot of other things to look at in the language. I spent two articles with twenty modules each going over modules in the standard library I found useful for certain things! That being said, there is a lot to unpack with Python. Not only in the ecosystem that it boasts, but also inside of its standard library and base. Although there is always a chance a reader might already know all of these Python features mentioned, without a doubt most likely do not know every aspect of the Python programming language. Perhaps some of these weird quirks and features of the language will come in handy in your next project!
If you are a C programmer, it is likely that you are familiar with return types. For all of the non-C programmers out there (or other languages with this feature, Java, C++, etc.) the return type is simply the type of the data that is going to be returned at the end of our function. Also, if you have interest in C and are not sure where to start after reading that, I do have an article that provides a simple introduction to C that is very friendly for newer programmers. If you desire that read, you may check it out here:
towardsdatascience.com
Some might actually be surprised to know that you may actually do this in Python, as well. I actually was on the boat of people that did not know about this until recently. Python is rather weird with the way it handles types and functions in the first place... Coming from a language like Julia with a robust, strong typing system, although Python is relatively strongly typed and has little implicit typing, it still strikes me as odd that specifying argument types does essentially nothing:
# This is weird.def add5(x : int):     return(x + 5)print(add5(5))10print(add5("Hi"))TypeError: can only concatenate str (not "int") to str
It is weird to me that the function tried to execute, even though we did not provide the correct type. The same weird thing applies to getting return types. In order to specify a return type, we use function annotation with ->.
def add5(n : int) -> int:    return "Hi " + str(n)
You might notice that although this function’s specified return type is an integer, this function returns a string. In languages like C, this would not fly. An interesting thing about this in C is that it would make an integer from a pointer. In other words, we are actually experiencing implicit typing in the C language. The following is a code example that demonstrates this:
#include <stdio.h>int main(){printf("Hello World");return "Hello";}
And compiling this code gives the following result:
main.c:15:12: warning: returning ‘char *’ from a function with return type ‘int’ makes integer from pointer without a cast [-Wint-conversion]15 |     return "Hello";|            ^~~~~~~Hello World...Program finished with exit code 0Press ENTER to exit console.
The thing about the Python example, however, is that the type is not implicitly changed. As a result, we are pretty much able to return a string from a function with the return type of integer:
type(add5(5))str
Which is so weird — because what exactly does specifying a return type do to our Python code? From this standpoint, objectively when it comes to the type system, it does nothing. I decided to do some research on this in order to find out exactly what happens whenever we specify a return type.
I did some reading of some PEP explanations on this feature over on Python.org in order to get a better understanding of this feature. If you would like to read what I read, you may do so here:
www.python.org
Basically all that this iterated back into my brain, however, is that this is an entirely pointless feature. It just seems weird that such a thing would be programmed into Python without having a function — type system that actually makes use of it. My only thought about how this might actually be useful is that perhaps it could help with compilation. There was a chance of that being the case until I read
“ ... That is, the parameter list can now be followed by a literal -> and a Python expression. Like the annotations for parameters, this expression will be evaluated when the function definition is executed. “
It seems like this feature is for the most part not all that useful, perhaps it could be used to generate some meta-data for doc-strings, i.e. it will make documentation easier to auto-generate by having the return-type there. Regardless of who or what this feature is actually for, it is certainly a weird one.
One thing I most certainly love when it comes to Python is decorators. Decorators are an incredibly valuable feature to Python in my opinion that give a huge plus to programming in the language. In all honesty, I am excited for the industry to move further away from Python because there are so many short-comings to the language, and I really do not feel the type system is very robust at all. That being said, one redeeming quality that I have not seen in many other programming languages is decorators.
Decorators are easily one of the simplest and easiest to use features in Python that can have quite a dramatic impact on the code you produce. They can be used to speed up code, change the way it works, and even manipulate how objects store data in Python. That being said, my next weird Python feature comes in the form of the caching abilities presented in the functools module from the standard library. Also, if you would like to learn more about this library, I have an entire article on it that is pretty radical:
towardsdatascience.com
In order to utilize caching, we will import lru_cache like so:
from functools import lru_cache
This decorator makes a function cache a certain number of calculations. Recursion can be a significant problem for computers to endure, and there are a lot of situations where one can avoid using a recursive function to solve something. However, one situation where it is very common to see recursive implementations is a factorial function.
def factorial(n):    return n * factorial(n-1) if n else 1
Let us try out our new factorial function with the number 15. I will also time this in order for us to get a grasp on how long the computer takes to do such an operation.
%timeit factorial(15)1.94 μs ± 68.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
Now let us try a factorial of a higher number, and we see this still takes forever:
%timeit factorial(20)1.94 μs ± 68.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
Now we will write the same function with a new definition, fact(). The only difference is that this time we will decorate our function with lru_cache.
@lru_cachedef fact(n):    return n * factorial(n-1) if n else 1
Now let us repeat the factorial of 15 calculation:
%timeit fact(15)63.8 ns ± 1.83 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
We see that there is essentially no difference between the calculation of this compared to the prior attempt without the caching. The real trick is when we now step up to the factorial of 20:
%timeit fact(20)64.1 ns ± 1.51 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
We see a significant improvement to calculation of this value. Why is this the case? The lru_cache function will save some of the calculations into cache. This makes subsequent calls already know the answer to the function call and simply return those values from its prior memory. This can be incredibly valuable, and this function is a great example of a prominent application. This would cut binomial distribution calculations in half in some instances.
Next on my list of weird Python is the dataclass. This is another decorator that I find to be incredibly valuable. Although the decorator is of course very simple in what it does, it can very effectively save a lot of lines of code by eliminating the need for initialization functions and other components of typical Python classes. If you would like to read more on this particular subject, I have an entire article on dataclasses and why I think they are cool you may read into here:
towardsdatascience.com
The whole point of a dataclass is to effectively make something more akin to a classic struct type in languages like C inside of Python. Given Python’s object-oriented nature, it is easy to see why it can sometimes get in the way of creating types that are simply meant to be data structures, rather than an entire class. Using dataclasses, one can effectively shield themselves from these object-oriented portions of Python for particular types by simply making a decorator call. Consider the following example:
@dataclassclass Food:    name: str    unit_price: float    stock: int = 0            def stock_value(self) -> float:        return(self.stock * self.unit_price)
Notice that in this instance there is no __init__() function. Typically, if we were to write this class and provide our values, name, unit_price, and stock, we would run into an error. This is because the class would actually take 0 arguments, not the three that we are attempting to provide. Needless to say, this can be problematic if we just want to make something more like a struct. This can also save a lot of lines of code if one does not have anything to do inside of initialization besides assign member variables. Here is a usage of the example I just talked about, where we will receive that the class takes no arguments:
class Other:    name: str    unit_price: float    stock: int = 0spaghetti = Other("Spaghetti", 8.75, 28)TypeError: Other() takes no arguments
But in the example of the Food class from before,
lasagna = Food("Lasagna", 10.26, 45)
Strings can at times be quite difficult to formulate and input different values precisely where desired. If I wanted to put a value into the middle of a string in Python, how exactly would I do that? Some might take a splitting approach, or perhaps try and insert after a particular phrase in the string — and these might be decent solutions. However, there is a better approach that one could have to solving this problem. Consider that I have the following two data components, my name and age:
name = "Emmett"age = 22
I want to print a string that will tell my name and age in a complete string. For the sake of an example, I am only allowed to initialize one more string type. How would I do this? Adding an f before our first quotation mark in the string definition will allow us to input values from the identifiers in our code using brackets {}, like so:
print(f"Hello, my name is {name} and I am { age }")Hello, my name is Emmett and I am 22
There is also another way that you can do this as well, like so:
print("I'm a {Ds} Named {nm}".format(Ds = "Data Scientist", nm="Bob"))I'm a Data Scientist Named Bob
There is certainly a multitude of occasions where something like this could prove to be useful. One example that comes to mind is of course in label processing. I happen to have an article all about label processing in Python, if you would like to learn more about that topic you may read that article here:
towardsdatascience.com
Python certainly does have a number of weird quirks and oddities, some of which are much more useful than the others. These are just some, of which there are many more. I hope that some of these weird little tracks happen to come in handy at some point in your software journeys! Thank you very much for reading my article. If one section stands out a lot to me in this article, it is likely the two decorators, as in my experience I have found them to be incredibly useful. I have an article that lists a bunch of different decorators I have found to be useful that I would highly recommend! You may read that here:
towardsdatascience.com
Thank you very much for reading my article. Good luck utilizing these strange additions to their maximum potential!