Date

After many years of doing Python, first as a hobbyist and then in a professional setting, I’ve had to do some amount of Go programming. Also, I’ve been trying to explore other languages, primarily Rust, but have been touching some C while following some projects ([1] and [2]).

There is value in learning from other languages for the sake of itself, one can end up deepening knowledge and getting enlightenment through it. I remember having that feeling while doing some Ruby stuff in a MOOC years ago. In this article, I want to explore some of the learnings I’ve had lately. For a comprehensive view on this topic and how it reflects mainly in Python this article is a great and insightful read.

Typing

At the beginning of my Go tenure, it was slightly strange to be in the realm of a statically typed language enforced at compile time. Over the years, I had had some experience with compiled languages (Pascal, C, C#, Java), but never in any serious way. However, I quickly started enjoying the clarity that static types can provide, mostly through what the type checking enables in an IDEs like Jetbrain’s Goland.

While working with Python alongside Go, I found myself missing the clarity and helping “hand” that the static typing offers and enables. I was aware of mypy and the whole effort to add some type annotation into Python, but I was observant on it. This experience with Go drove me to start using mypy. In general, I’ve found it to be a great tool, and I’m trying to annotate as much as possible. It was a great help when doing a large scale refactoring/recycling of code, and in other instances, it has helped me to find some latent bugs “hidden” in the code. On the other hand, sometimes it can be slightly tricky to annotate arguments and variables accurately. Personally, I haven’t found the typing system in Python to be wrong or particularly bad as some people in the community have expressed, most likely because the things I’m doing are rather straightforward and don’t use really any metaprogramming or abuse the dynamic nature of Python (too much).

from Go import

Additionally, from Go, I did pick up some of the implicit and explicit conventions of the broader community. The most prominent ones that come to mind being - Avoid the use of frivolous else statements (link) - Use gofmt to format code as a standard - Conditional assignments (link)

The first point in the list is rather easy to implement while writing Python code. Although try-except blocks make the implementation of this idea to be slightly more indented. But one can still aim to use such a style pattern when possible.

For the second point, there are some tools like autopep8, yapf, and black. I personally favor the last one because it is intentionally designed to have very few options that the user can control. You might like or not the decisions, like with gofmt, but it keeps a high degree of consistency across different code bases and liberates you of trying (and failing) to follow a particular style. And I certainly prefer not having to worry and have some uniformity than pleasing my personal sense of code aesthetics. After some months of using blackened code, it’s hard for me to read messily styled code.

Now, regarding the last one, I’m waiting for Python 3.8 where the expression assignment operator is being included. A simple example from PEP-572

# Python 3.7
env_base = os.environ.get("PYTHONUSERBASE", None)
if env_base:
    return env_base

# Python 3.8
if env_base := os.environ.get("PYTHONUSERBASE", None):
    return env_base

This one surprised me because it’s not something I ever thought that I could need, but once I got to use it in Go, then I kept finding places where I’d have used it in Python.

from _ import

Since this post is not called something like “taking Go teachings to Python”, then it is time to introduce another language. As mentioned in the opening of this article, in my reduced spare time, I’ve been exploring Rust as it seems like a very complementary tool to have. I’ll not explain why Rust is cool and worthy, but I’ll mention that it has something extremely interesting that touches the topic of type annotations in Python.

Rust has a Result<T, Err> and Option<T> types” that are used as return “values” of functions. They encode different possibilities of returned values.

In the first case, Result<T, Err>, a composite “object” is returned that either holds a value of type T or an error. Somehow, in my opinion, it feels rather similar to what Go 1.X does for error handling.

// Go
x, err := double_number("10")
if err {
    fmt.Println("Error: ", err)
} else {
    fmt.Println("Doubled number: ", x)
}
// Rust
match double_number("10") {
    Ok(n) => println!("Doubled number: {:?}", n),
    Err(err) => println!("Error: {:?}", err),
}

This doesn’t have a direct equivalent in Python since it has exceptions instead of error values/types, but one could certainly make a similar pattern than the one in Go by catching exceptions and returning some value instead.

The second, Option<T>, encodes a value of type T or something akin to None in Python. Taking the example from the documentation (playground)

fn divide(numerator: f64, denominator: f64) -> Option<f64> {
    if denominator == 0.0 {
        None
    } else {
        Some(numerator / denominator)
    }
}

// The return value of the function is an option
let result = divide(2.0, 3.0);

// Pattern match to retrieve the value
match result {
    // The division was valid
    Some(x) => println!("Result: {}", x),
    // The division was invalid
    None    => println!("Cannot divide by 0"),
}

After studying Rust a bit, I started to use the Optional type annotation in Python more often to explicitly signal that a function/method returns a value or None. Certainly returning or receiving a None in Python is something rather frequent, but formalizing it in a type makes it very explicit and clear. Taking the previous Rust example ported to Python, it’d look like the following

from typing import Optional

def divide(numerator: float, denominator: float) -> Optional[float]:
    if denominator == 0.0:
        return None
    return numerator / denominator

result = divide(2.0, 3.0)

if result:
    print(f"Result: {result}")
else:
    print("Cannot divide by 0.")

Probably not the best example, but it shows how to apply this same pattern in a properly annotated form. Annotating and then running mypy will force you to cover your bases, thus reducing some silly bugs in the code.

Quite interestingly, I found this post showing some sort of implementation of these two ideas in Python. It’s worthy of checking it out, a very interesting experiment! I hope to try it out at some point.

Furthermore, the Optional type can also be used for argument annotations for arguments that might have a particular value of a type or None, as it could be when they have None as a default.

from typing import Optional

def fib(n: int) -> int:  # declare n to be int
    a, b = 0, 1
    for _ in range(n):
        b, a = a + b, b
    return a

def cal(n: Optional[int]) -> None:
    print(fib(n))

If mypy is run over this code, it’ll complain at the print(fib(n)) line saying Argument 1 to "fib" has incompatible type "Optional[int]"; expected "int". It’s clear, at that point n could be some integer or None. This forces us to assure that n is actually a number beforehand and thus avoiding some ill-defined call and a highly likely runtime error. Thus, the annotation error in the previous code is fixed by modifying in a verbose mode the cal function as follows

def cal(n: Optional[int]) -> None:
    if n:
        print(fib(n))
    else:
        print("No number provided!")

In my opinion, mypy has been a great tool when working on a moderately sized project. Others of the like of Dropbox and Instagram have found it invaluable in their efforts to move from Python 2 to Python 3.

Closing remarks

Go and explore other languages! It will most likely provide great insight into your preferred language and your own programming style. There is no need to be an expert, but already, the challenge of understanding a new language provides plenty of teachings.

And regarding Python itself, use black and annotate code, particularly the code that gets into production and is being produced by a team.


Comments

comments powered by Disqus