Make your Python functions return something meaningful, typed, and safe
returns.readthedocs.io
returns.readthedocs.io
To add insult to injury, type hints are not enforced nor even verified by the runtime. I find that if I want type safety, I'd rather branch out to other languages that were designed for it from the start. In particular, I find that rewriting some of my Python modules in Rust is a joy (using PyO3, but keenly following RustPython development too).
I only typehint certain parts of the code where i find it usefull. Typehinting everything makes Python code really hard to read IMO.
- tests fail faster
- code documentation
- IDE’s complain better
- teams collaborate better
It becomes more helpful as projects and teams mature.I’d say that types aren’t the last step though, I’m starting to prefer protocols and those are proving to be even more loosely coupled and more pythonic to me. They let you “type” the behavior rather than the actual type. Say there is a case when you don’t care if it’s a set, tuple, or a list because all you’re going to do is Iterate, we’ll you’d go the behavior route and say you want an Iterator. You can create your own protocols as well and it has the effect that you document what you intend to do with the objects passed to you such that it doesn’t matter what you pass as long as it adheres to the protocol you defined. It’s documented duck typing. Here’s a great video that explains how it can simplify your code https://m.youtube.com/watch?v=xvb5hGLoK0A
Why waste time doing static analysis each time you run the code? If you don't change the code, its not going to change.
Currently they are little more than a crutch to have intellisense working.
Those other languages don't have pandas, scikit-learn, pytorch. If you are building on top of Python's data science and machine learning ecosystem, it's nice to have both the wide range of libraries available in Python, and type safety. Even if the annotations get a bit ugly looking.
If the types are really complicated, then the function probably is too.
def calculate_points(word: str) -> int:
guessed_letters_count = len([letter for letter in word if letter != '.'])
return _award_points_for_letters(guessed_letters_count)
def _award_points_for_letters(guessed: int) -> int:
return 0 if guessed < 5 else guessed # minimum 6 points possible!
this is an enormous improvement. def calculate_points(word: str) -> int:
count = sum(x != '.' for x in word)
return _award_points_for_letters(count) def _award_points_for_letters(guessed: int) -> int:
return 0 if guessed < 5 else guessed # minimum 6 points possible!
vs def _award_points_for_letters(guessed):
return 0 if guessed < 5 else guessed # minimum 6 points possible!
is "guessed" letters or is it the number guessed?But if you don’t like to use any of it you don’t have to. So you can annotate just some code, either because you want the types checked or for performance reasons.
One caveat though: SBCL’s treatment of type declarations in this manner is implementation specific (well, SBCL’s predecessor, CMUCL, also did this.) The Common Lisp standard simply says the result is undefined if a value’s type does not match the declaration. So a more portable way to do this is to use the CHECK-TYPE macro and use declarations only for performance reasons.
Lets not make Python the new Java. People already start to translate design patterns 1 to 1 from Java examples to Python and I have to tell them, that there are other ways in Python. Lets not choke Python programs with design patterns that are needlessly implemented in complicated ways, where the language already provides other means! (singleton, visitor, factory, decorators, etc.)
if user is None:
return
balance = user.get_balance()
if balance is None:
return
credit = balance.credit_amount()
# etc...
Mypy also correctly detects that user/balance/etc. can't be null after each check. Of course it's better to design functions/methods that never return "None", but that's not always an option. This approach also has the advantage that you can easily log at what stage the sequence of method calls failed. Stuffing this into a bunch of lambdas makes that much harder.Business processes are rarely set in stone and the above code can be easily modified, as opposed to a "beautiful" "pure" "correct" ball of functional yarn that must be carefully unwound and rearchitected.
user
|> mmap(get_balance)
|> mmap(get_credit_amount) discount_program: Maybe['DiscountProgram'] = Maybe.from_optional(
user,
).bind_optional( # This won't be called if `user is None`
lambda real_user: real_user.get_balance(),
)
Ugh. I don't care that this is shorter – just use a bloody if statement!Yes, even if you have to nest them sometimes (not nearly as often as this page suggests in my experience).
But the Maybe monad does have its beauty, which the in-built Optional doesn't really provide. If this package makes people more accepting, then let them.
Good software isn't art.
https://www.merriam-webster.com/dictionary/beautiful
https://www.merriam-webster.com/dictionary/aesthetic
I'd rather optimize for substance than style.
Python allows recursion too. Are you using recursion? Why not just write it in Haskell?
Python allows function calls too. Are you using function calls? Why not just write it in a functional language like Haskell?
A monad is just a general programming concept, like recursion and function calls. One particular language pioneering the concept's heavy use doesn't mean other languages can't adapt it. To the contrary, languages get influences from one another all the time. Especially Python.
Do you like list comprehensions? This super-pythonic thing? Goes back to functional languages in the 70s and the name was coined by Philip Wadler, one of the main Haskell gurus. If you like list comprehensions, this feels like trying to write Haskell code in Python. Why not just write it in Haskell?
Here's why I dislike it. There's a lot of libraries out there in Python that appear to be DSLs or have custom syntaxes that heavily use decorators etc e.g. Flask or FastAPI. In exchange for learning these custom syntaxes they offer tangible value in terms of eliminating boilerplate or solving a particular use case really well. In case of Flask or FastAPI, they make writing http/rest servers a piece of cake.
This library however offers not much benefit over the following example from Effective Python and in return I have to learn a whole new Python.
def add_ints(a, b) -> Tuple[str, bool]:
if isinstance(a, int) and isinstance(b, int):
return a + b, True
else:
return None, False
result, status = add_ints(1, 1)
Basically it's is a lot of syntactic noise for very little.def add_ints(a: int, b: int) -> Tuple[int | None, bool].
I’m not a fan of the Tuple return type. This is how golang does things and while it works there I don’t like seeing this in Python code because the Tuple tells you nothing unless you follow this convention and you have to add the Tuple annotation on top of every return type making it harder to read. I don’t see a need for it here since you’re already returning an Optional[int] (int | None in Python 3.10+). You could also raise a TypeError. Also your type checker should catch this if a or b are not an int, so you might feel safe returning a+b at this point and having the return type be simply int.
from returns.result import Result, Success, Failure
def find_user(user_id: int) -> Result['User', str]:
user = User.objects.filter(id=user_id)
if user.exists():
return Success(user[0])
return Failure('User was not found')
user_search_result = find_user(1)
# => Success(User{id: 1, ...})
user_search_result = find_user(0) # id 0 does not exist!
# => Failure('User was not found')
https://returns.readthedocs.io/en/latest/pages/result.htmlThe lambda issue in Python isn’t really about lambdas, but the readability and expressiveness of expressions.
OTOH, small improvements in what can cleanly be expressed in an expression could still have big payoffs for Python.
Which languages are those? Java, C#, C++, Rust, JS, Swift, Kotlin, Scala, Ruby, PHP, and Go all allow defining multi-line anonymous functions that can contain any statement normally allowed in a function.
I consider Python as the lingua francua that gets the job done, easy to train new programmers to use it and there are several C/C++/Fortran/Rust libraries written so it can do things performant enough to be relevant. There was nothing new introduced by Python that other languages took from it which shows that there is nothing groundbreaking about it.
I think Python is the quick and dirty, not too smart glue languages with a mediocre module system and many footguns. I think Sussman pointed out that the world needs more library gluers than it needs very smart software engineer who create those libraries. Python is the proof of this theory.
Not strictly true, Python's approach to readability (syntax) inspired languages like Kotlin, GDScript, to name a couple.
But the bigger picture is that Python has spawned prominent frameworks/DSLs in virtually every domain, so to suggest that it is a dead-end of innovation is not quite correct.
edit: I like Scala. Being emulated is a compliment. Come on guys, no need to throw a fit!
Raise a `NotFound` exception in the method and catch that instead when you're calling...
# Assuming credit can't be negative.
if user and (balance := user.get_balance()) and (credit := balance.credit_amount()):
discount_program = choose_discount(credit)All these concepts are great and I use them all the time... but just not in Python.
Every language has tradeoffs in terms of features, available libraries, existing codebases you need to work within, etc. Python has a tradeoff of not having these functional features.
I appreciate the huge amount of effort this must have been, but these types of projects don't usually work out or stick.