🐍 Python - Functions
Function declaration.
def add(x: int, y: int) -> int:
return x + y
assert add(2, 3) == 5
assert add(y=3, x=5) == 8
Lambdas are anonymous functions with a single expression. If you find a lambda that is hard to understand, refactor it into a normal function. A well-defined function always beats a lambda. Lambdas are so bad that the Python standard library even has operator
module that contains usual stuff you would use a lambda for.
from typing import Callable
f: Callable[[int], bool] = (lambda x: x > 2)
assert f(3) is True
assert f(2) is False
add: Callable[[int, int], int] = lambda x, y: x + y
assert add(5, 3) == 8
tuples = [(1, 'd'), (2, 'b'), (4, 'a'), (3, 'c')]
print(sorted(tuples, key=lambda x: x[1]))
# => [(4, 'a'), (2, 'b'), (3, 'c'), (1, 'd')]
Python has first-class functions. Functions can be stored to variables and they can be passed around.
from typing import Callable
funcs = [str.lower, str.upper]
print(funcs[0]('TiCk ToCk'))
# => tick tock
for f in funcs:
print(f, f('TiCk ToCk'))
# => (<method 'lower' of 'str' objects>, 'tick tock')
# => (<method 'capitalize' of 'str' objects>, 'TICK TOCK')
def greet(func: Callable[[str], str]) -> None:
greeting = func('Hi, I am a Python program')
print(greeting)
greet(funcs[1])
# => HI, I AM A PYTHON PROGRAM
print(list(map(funcs[1], ['hello', 'hey', 'hi'])))
# => ['HELLO', 'HEY', 'HI']
def create_logger(name: str) -> Callable[[str], None]:
def logger(message: str) -> None:
print('%s: %s' % (name, message))
return logger
log = create_logger('Controller')
log('SNAFU!') # Controller: SNAFU!
Always consider using built-in functions. They are written in C so they are fast.
# http://docs.python.org/library/functions.html
def add_10(x: int) -> int:
return x + 10
assert list(map(add_10, [1, 2, 3])) == [11, 12, 13]
assert list(filter(lambda x: x > 5, [3, 4, 5, 6, 7])) == [6, 7]
Objects become callable if you define a __call__
function.
class Adder:
def __init__(self, n: int) -> None:
self.n: int = n
def __call__(self, x: int) -> int:
return self.n + x
assert callable(Adder)
plus_3 = Adder(3)
assert callable(plus_3)
assert plus_3(4) == 7
import random
from typing import Any, Iterable, List
class BingoCage:
def __init__(self, items: Iterable[Any]) -> None:
self._items: List[Any] = list(items)
random.shuffle(self._items)
def __call__(self) -> Any:
try:
return self._items.pop()
except IndexError:
raise LookupError(f'call to empty {self.__class__.__name__}')
bingo = BingoCage(range(3))
assert callable(bingo)
assert isinstance(bingo(), int)
assert isinstance(bingo(), int)
assert isinstance(bingo(), int)
# assert isinstance(bingo(), int) # => LookupError: pick from empty BingoCage
Learn about *args
and **kwargs
. *args
and **kwargs
allow you to pass a variable number of arguments to a function. Only the *s are necessary. Writing *args
and **kwargs
is just a convention.
Optional Arguments
You define a default value for optional arguments.
def greet(text: str = 'Hello') -> str:
return f'{text}!'
assert greet() == 'Hello!'
assert greet('Moi') == 'Moi!'
Use labelled parameters with default parameters. Always use labelled parameters when calling a function with default parameters. Helps extendibility when adding new default variables.
def foo(a: int, b: int = 1, c: int = 2) -> None:
pass
foo(10)
foo(10, b=20)
foo(10, c=30)
foo(10, 30) # BAD
Don't use mutable variables in default arguments. None
should be reserved for "argument wasn't passed" in these cases. The most common bugs are using lists or dictionaries as a default argument.
# BAD, BAD, BOY
from typing import Iterable, Optional
def bad(numbers: Iterable[int] = []) -> Iterable[int]:
numbers.append(123)
return numbers
assert bad() == [123]
assert bad() == [123, 123]
assert bad() == [123, 123, 123]
# so don't do this
# GOOD
def good(numbers: Optional[Iterable[int]] = None) -> Iterable[int]:
if numbers is None:
numbers = []
numbers.append(123)
return numbers
assert good() == [123]
assert good() == [123]
assert good() == [123]
Don't use a result of a function as a default function parameter. It will only use the value that it returned when it was defined.
import time
# bad
def print_now(now: float = time.time()) -> float:
return now
n = print_now()
assert print_now() == print_now() == print_now()
assert print_now() == print_now() == print_now()
assert n == print_now()
Clone mutable variables in your class constructors. If you don't, internals of the class can be modified outside of the class.
from typing import Iterable, List
class Bus:
passengers: List[str]
def __init__(self, passengers: Iterable[str] = None) -> None:
if passengers is None:
self.passengers = []
else:
self.passengers = list(passengers) # the cloning
def pick(self, name: str) -> None:
self.passengers.append(name)
def drop(self, name: str) -> None:
self.passengers.remove(name)
team = ['Jordan', 'Sue', 'Alex']
bus = Bus(team)
assert bus.passengers == ['Jordan', 'Sue', 'Alex']
bus.drop('Alex')
assert bus.passengers == ['Jordan', 'Sue']
assert team == ['Jordan', 'Sue', 'Alex']
# so team and bus.passengers are separate, good
Undefined Arguments
Pass undefined arguments with *args
and **kwargs
The names args
and kwargs
are just a convention, follow that naming if it makes sense.
def foo(required, *args, **kwargs) -> None:
print(required)
if args:
print(args)
if kwargs:
print(kwargs)
foo('I am required', 'I am optional', john='I am optional and I have a name')
# => I am required
# => ('I am optional',)
# => {'john': 'I am optional and I have a name'}
You can modify the undefined arguments before passing them forward. Passing optional arguments forward is especially useful with subclasses and decorator functions.
def foo(required, *args, **kwargs) -> None:
print(required)
if args:
print(args)
if kwargs:
print(kwargs)
def bar(required, *args, **kwargs) -> None:
new_args = args + ('something-extra', )
kwargs['person'] = 'Bob'
foo(required, *new_args, **kwargs)
bar('I am required', 'I am optional', john='I am optional and I have a name')
# => I am required
# => ('I am optional', 'something-extra')
# => {'john': 'I am optional and I have a name', 'person': 'Bob'}
You can mix-and-match named arguments and argument unpacking.
from typing import Optional
def tag(name: str, *contents, cls: Optional[str] = None, **attrs) -> str:
if cls is not None:
attrs['class'] = cls
if attrs:
attrs_str = ''.join(f' {attr}="{value}"' for attr, value in sorted(attrs.items()))
else:
attrs_str = ''
if contents:
return ''.join(f'<{name}{attrs_str}>{c}</{name}>' for c in contents)
else:
return f'<{name}{attrs_str} />'
assert tag('br') == '<br />'
assert tag('p', 'hello') == '<p>hello</p>'
assert tag('p', 'hello', 'world') == '<p>hello</p><p>world</p>'
assert tag('header', style="color: red;") == '<header style="color: red;" />'
assert tag('div', 'John', id=123) == '<div id="123">John</div>'
assert tag('footer', cls="small") == '<footer class="small" />'
Function Returns
Python functions return None
if nothing else is specified.
def i_return_none():
pass
assert i_return_none() is None
Add explicit return None
to functions with one or more return
keywords. Using implicit None
is misleading and error-prone in functions with multiple possible returns.
from typing import Optional
# BAD, even the type is wrong in here
def foo(name: str) -> str:
if name:
return name.upper()
# GOOD
def bar(name: str) -> Optional[str]:
if name:
return name.upper()
return None
You can return multiple values. Returning multiple values in Python wraps the values in a tuple. You can easily unpack them with Python multiple assignment.
from typing import Tuple
def get_full_name() -> Tuple[str, str]:
return 'Ruksi', 'Korpisara' # implicit cast to a tuple
assert get_full_name() == ('Ruksi', 'Korpisara')
first_name, last_name = get_full_name() # multiple assignment
assert first_name == 'Ruksi'
assert last_name == 'Korpisara'
Consider returning self
to allow function chaining. But be consistent with this behavior.
class Reactor:
def release_water(self) -> 'Reactor':
print('releasing water')
return self
def shutdown(self) -> 'Reactor':
print('shutting down')
return self
def alarm(self) -> 'Reactor':
print('alarm triggered')
return self
r = Reactor()
r.release_water().shutdown().alarm()
If chaining doesn't make sense, explicitly return None
. Functions that change an object in place should return None
to make it clear that the object itself was changed.
from typing import List
def add_person(people: List[str], person: str) -> None:
people.append(person)
return None
people = ['Alice', 'Bob']
assert add_person(people, 'John') is None
assert people == ['Alice', 'Bob', 'John']
Sources
- Style Guide For Python Code
- Django Coding Style
- Google Python Guide
- Python Newbie Mistakes
- Learn Python in Y minutes
- Python’s null equivalent: None
- Python Idioms
- Python Best Practice Patterns
- Become More Advanced: Master the 10 Most Common Python Programming Problems
- Python Tricks The Book, Dan Bader
- Fluent Python, Luciano Ramalho