My Thoughts on Python (2025)
Posted on: 2025-01-31
My first hand with Python was between 2018 and 2021 when I studied machine learning master's degree. Last year, I started building a Discord Bot which increased in size in the last few months. Here are my thoughts.
Typing Is Very Weak
Python introduced types in 2015 in Python 3.5. During my assignments I never used them. Mostly because we had the requirement to use Python 2. As I developed more and more features in my project, I forgot exactly which type of parameters or variables I needed and added types. Now, the whole code is fully typed. Still, I have many runtime issues. Coming from a similar experience where JavaScript wasn't typed and TypeScript added type I was surprised to see that typing the code does not provide much defense against bad code. Python's type is mainly a documentation and helps the code editor.
def get_reactions() -> List[str]:
"""
Returns a list of all the emojis that can be used to vote.
"""
return EMOJI_TO_TIME # Good code is: return list(EMOJI_TO_TIME.keys())
I understand that Python does not have a compilation step and, thus, cannot raise the issue before runtime. However, I am surprised that VsCode with the official Python extension does not mark the invocation of a function with wrong parameter types as an issue. As I was refactoring and moving code around, I found myself in a situation where I was failing. Fortunately, I have several unit tests that mitigate most of the issues. However, the problem with testing was that the tests were failing not because of assertion but because the PyTest tool couldn't run the tests in some situations because of the refactoring of broken dependencies. In other cases, the unit tests would fail before hitting the assertion because a function received a wrong object and properties were missing.
Another pitfall with type is asynchronous functions. Missing an await
generates an exception instead of automatically giving the developer a hint that this does not work.
In the end, the problem is that once it works, it works but nothing prevents someone from modifying the return of a function and still having the code looking fine until at runtime it fails. While unit tests might mitigate some issues, there are many scenarios where it continues to be precarious. Going back to the change of the return type, the unit tests of the modified function might fail and will require changes but every place where the code invokes the function is now failing. In the advent that one caller of the function does not have a test that invokes the function then it won't be visible that the developer needs to modify the code. Furthermore, if the function was mocked then the mocked return continues returning the wrong type and the tests that call the now modified function will not fail.
Unit Tests and Mocking are great
In the last few years, TypeScript and Jest have made testing much easier than in previous frameworks. However, I still struggle in some situations where one part of a file needs a few mocks while the other does not. For example, it is challenging if File1.ts
has two functions and you want f1
to mock f2
because f2
should not be invoked for some specific tests. With Python and PyTest, it is a breeze, and testing any function can mock any others regardless of where the function lives.
Here is an example:
@patch.object(bet_functions, bet_functions.data_access_create_bet_user_wallet_for_tournament.__name__)
@patch.object(bet_functions, bet_functions.data_access_get_bet_user_wallet_for_tournament.__name__)
def test_get_wallet_for_tournament_no_wallet_create_one(mock_get_user_wallet_for_tournament, mock_create_bet):
"""
Test the user scenario that a user does not have a wallet
"""
# Arrange
mock_get_user_wallet_for_tournament.return_value = None
mock_create_bet.return_value = BetUserTournament(67, 44, 12, 100)
# Act
get_bet_user_wallet_for_tournament(44, 12)
# Assert
mock_create_bet.assert_called_once_with(44, 12, DEFAULT_MONEY)
mock_get_user_wallet_for_tournament.assert_called()
assert (
mock_get_user_wallet_for_tournament.call_count == 2
) # One time for the None and one time for the created wallet
Python Package Management is Fine
I use pip
and a virtual environment (.venv
). Similar to NPM, you fetch your package and have a requirements.txt
that contains the list of dependencies. I am running the code in production, which ensures fetch using Pip for all the dependencies and running the code. It works. But, it can get confusing if you use Conda (or Anaconda) because it allows you to have compiled version of libraries. I recall doing all my school work using Conda, but in hindsight, I could have used only pip
. Someone could argue that JavaScript/TypeScript also has many package managers like NPM, Yarn, pnpm and that is true. In that category, the best experience seems to be Rust using cargo
: a single place and easy-to-upload and download package.
# Create Environment
python3 -m venv .venv
# Activate Environment
source .venv/bin/activate
# Update PIP
python3 -m pip install --upgrade pip
# Install Requirements
python3 -m pip install -r requirements.txt
The only issue I had was that I wanted to develop on a second computer, and for some reason, the dependencies of some dependencies were failing to install. The version wasn't available. I had to call Pip on all dependencies and have a slightly different list of versions.
Python Coding Style
Indeed, I have a strong bias toward languages like TypeScript, Java, and .Net since I created most of my code using these languages. However, my past with PHP helps me appreciate Python. I enjoy a few syntactic shortcuts like list comprehension but found that there are some illogical directions in the way I write. For example, in JavaScript (or similar language), to get the length of an array, you need to go into the object and find the functionality. It is logical, and you have a great sense of discovery.
const size = myArray.length(); // JavaScript
size = len(myArray) // Python
Similarly if you want to do something to each entry of an array.
const double = myArray.map(d=>d*2); // JavaScript
double = list(map(lambda x: x * 2, a)) // Python
The Python version returns an iterable, which requires using list
to build a new list. Also, the map
function needs an anonymous function that takes the transformation first, then the original array. This is the reverse order of JavaScript. While it is more a matter of preference, I found myself having to search more in the documentation or search on the web to find the right function to accomplish the task instead of relying on autocomplete when using other languages.
Import Automation
I haven't yet figured it out, but VsCode can find that a type or function is in your project and can suggest an import. Great! However, in my project, VsCode never adds the first folder. For example, reusable logic in my codes are inside deps
folders for inner dependencies, and VsCode will automatically add:
- from tournament_data_class import Tournament
+ from deps.tournament_data_class import Tournament
The automation of import is not as great in my current project but that might be an issue in the way I have setup my folders and files. Still, it remains that it is an irritation that the ecosystem does not guide me. I'll have to investigate later and see what potential solution is applicable.
Simple Development and Runtime
Getting started, developing, and releasing is very smooth. It is the opposite of Java with the installation of heavy developer kits and editor configurations. I enjoy that most computers already have Python, and creating a virtual environment is a single command. Running in production Python as a service has very few commands. Similarly, using VsCode was quick and a single extension. Debugging the project was without issue, which is not always the case with the TypeScript project. The experience if similar to Rust and it is frictionless.
Good Third-Party libraries
Python has a large third-party library pool. Using NumPy
and Pandas
for data manipulation that leverages fast C++ libraries is well documented. Creating images or plots with MatPlotlib
is popular online with many examples. Performing calls to processes outside the main application, like FFmpeg. The large amount of example online and the combination with AI tools make using Python a breeze to learn.
Good Date and Time Support
Python is easy to use for date, time, and timezone. Many utility functions are included to manipulate time. I found the experience refreshing without the need for a third-party library to handle different timezones and add/remove time to date.
Conclusion
Overall, the typing situation looks problematic as a project grows, especially if you have not written a part (or don't remember). The project is only about 7,000 lines of code, and I feel some pain and hesitation when refactoring. With a stronger typing system, moving files from one folder to another would let the code editor manage the reference automatically. Similarly, when breaking down functions and types into multiple ones, there would be a way to ensure that the code remains valid without running unit tests and then having runtime exceptions. I'm not saying Python cannot be used for big projects because it was, but receiving quicker feedback if you are passing the wrong object type or parameter count should be a few milliseconds (the editor should tell). Similarly, a large code base with thousands of unit tests is slow to run, and having to go back and forth until it compiles and runs successfully can be improved by compilation steps (or could be a good linting that does not compile to an output). I still enjoy coding in Python and will keep this project in Python, similar to my transition from JavaScript to TypeScript (10 years ago!) I believe that strongly typed languages ease development by increasing the speed of understanding a piece of unknown (or forgotten) code but also ease how easy it is to modify code with certainty of the impact.