Disclaimer: This list is meant as a comprehension of cool tips and tricks I found on the internet. If you would like to contribute, or notice any mistakes or typos, please contact me or upload a pull request. If you think any material here can be considered personal property let me know and I will take it down.
Using range()
is better than using a list (ex. [1, 2, 3]
), because the list takes up memory space, whereas the range()
function generates values on demand, thus taking a fixed amount of memory whatever the size of the elements is:
for i in range(10):
print(i**2)
takes the same memory space as:
for i in range(100000):
print(i**2)
Note: This function used to create a list in python2, and xrange()
used to do what range()
currently does, but it got changed, so range()
in python3 is xrange()
in python2.
Use the function reversed()
:
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
for lang in reversed(langs):
print(lang)
# prints rust, kotlin, c++, java, python, c
Use the function enumerate()
:
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
for i,lang in enumerate(langs):
print(f'{i} --> {lang}')
# prints
# 0 --> c
# 1 --> python
# 2 --> java
# 3 --> c++
# 4 --> kotlin
# 5 --> rust
Use the function zip()
. It returns tuples of the elements until one of the iterables is exahusted:
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
numbers = [1, 2, 3]
for number, lang in zip(numbers, langs):
print(f'{number} --> {lang}')
# prints
# 1 --> c
# 2 --> python
# 3 --> java
Note: It takes any number of iterables and "zips" them into tuples.
Note 2: It's important to note that it generates tuples on demand, so it reuses memory space (it used to create a third list in python2, and izip()
used to do what zip()
does now in python3).
Use the function sorted()
or the method sort()
of iterables.
By default, it sorts the iterable in ascending order:
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
for lang in sorted(langs):
print(lang)
# or
langs.sort()
print(*langs)
# prints c c++ java kotlin python rust
The second method sorts the iterable IN-PLACE, whilst the first returns a different iterable.
Both the functions can take 2 extra parameters which can specify a comparison function and if the iterable should be reversed:
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
for lang in sorted(langs, key=len, reverse=True):
print(lang)
# or
langs.sort(key=len, reverse=True)
print(*langs)
# prints kotlin python java rust c++ c
# notice that kotlin and python might be interchanged because they have the same size, same java and rust
A partial function is a function who has some parameters "frozen", in the sense that they are preset. The other parameters must be given when the partial function is called:
from functools import partial
def func(x, y, z):
return x + 2*y + 3*z
my_func = partial(func, 2, 3) # assign (preset) 2 to x, 3 to y
my_func(3) # equivalent to func(2, 3, 3)
# prints 17
my_func(4) # equivalent to func(2, 3, 4)
# prints 20
Going from fastest to slowest, they are:
f'{s} {t}' # fastest
s + ' ' + t
' '.join((s, t))
'%s %s' % (s, t)
'{} {}'.format(s, t)
Template('$s $t').substitute(s=s, t=t) # slowest
Note: f-strings were added in Python 3.6.
This method has 2 forms:
iter(iterable)
- this form simply returns an iterator from the iterable. You can callnext()
on the iterator and iterate through the iterable.
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
ir = iter(langs)
print(next(ir)) # prints c
print(next(ir)) # prints python
iter(callable, sentinel)
- this form executes the functioncallable
until it returnssentinel
value.
def func(langs = []):
langs.append('c')
return len(langs)
ir = iter(func, 5)
next(ir) # prints 1
next(ir) # prints 2
next(ir) # prints 3
next(ir) # prints 4
next(ir) # raise StopIteration
Read 80 characters from file f
into line
and append to text
until f.read()
returns ''
:
text = list()
for line in iter(partial(f.read, 80), ''):
text.append(line)
Search a certain value in an iterable and do something if it is not there:
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
for lang in langs:
if lang == 'scala':
print('We found Scala!')
break
else:
print('Scala is not in the list...')
Note: Careful if you ever come back to this kind of code. Don't indent the else
statement by accident!!!
d = {
'foo': 'c',
'bar': 'java',
'baz': 'rust'
}
# cannot mutate dictionary here
for k in d:
print(k)
# free to mutate the keys and values
for k in list(d.keys()):
if k == 'foo':
del d[k]
Note: d.keys()
used to make a list copy of the keys, so there was no problem iterating and mutating the original dictionary at the same time. In modern Python3, d.keys()
returns an iterable and can no longer be used to iterate and mutate a dictionary at the same time. To go around this, just wrap the method into a list as in the example.
Note 2: There is an 'alternative' to this, but it has worse performance and memory usage:
d = {
'foo': 'c',
'bar': 'java',
'baz': 'rust'
}
# Don't do this, performance is bad as it copies every element in a dictionary and can be really bad for really big dictionaries
s = {k: v for k, v in d.items() if k != 'foo'}
d = {
'foo': 'c',
'bar': 'java',
'baz': 'rust'
}
for k, v in d.items():
print(f'{k} --> {v}')
The items()
method returns and iterator, so it uses the same amount of memory no matter how big the dictionary is.
Note: In python2, the items()
method used to return a list of tuples, and the iteritems()
used to do what items()
does now in python3.
Use the zip()
method to pack 2 iterables into a zip object, then use the dict()
method to make that into a dictionary.
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
colors = ['blue', 'green', 'red']
d = dict(zip(langs, colors))
print(d) # prints {'c': 'blue', 'python': 'green', 'java': 'red'}
s = dict(enumerate(colors))
print(s) # prints {1: 'blue', 2: 'green', 3: 'red'}
Use the defaultdict()
method imported from collections
. When a key is not in the dictionary, it creates a new key that has the default value.
from collections import defaultdict
colors = ['red', 'green', 'red', 'blue', 'green', 'red']
d = defaultdict(int)
for color in colors:
d[color] += 1
print(dict(d)) # prints {'blue': 1, 'green': 2, 'red': 3}
Note: This is a faster approach than setdefault()
on most cases and faster than get()
in all cases. Also, defaultdict()
seems to work faster on native types like int
or string
and slower on dict
or list
. That being said, there are times when you cannot use defaultdict()
and have to use either setdefault()
or get()
, for example when the default value of a certain key depends on the key itself, so defaultdict()
cannot be used from the beginning to have a default value for every new key.
colors = ['red', 'green', 'red', 'blue', 'green', 'red']
d = {}
for color in colors:
d[color] = d.setdefault(color, 2 if color == 'red' else 0) + 1
print(d) # prints {'blue': 1, 'green': 2, 'red': 5}
Note 2: A case where get()
accomplishes nicely what setdefault()
and defaultdict()
would do in a more complicated manner is when you have to return a default value from a dictionary if the key is not in it.
d = {
1: 'Alice',
2: 'Bob',
3: 'Carla'
}
def hello(id):
return f'Hi, {d.get(id, "random person")}'
print(hello(1)) # prints Hi, Alice
print(hello(4)) # prints Hi, random person
Say you want to create a list with 100 elements of 0. You can just do:
lst = [0] * 100
print(lst)
This:
#include <stdio.h>
int main() {
int x;
int y = 1;
x = (y == 1 ? 1 : 0);
printf("%d", x);
return 0;
}
can be written like this in python:
y = 1
x = (1 if y == 1 else 0)
print(x)
Say you want to group the items in a list based on some comparison function, for example len()
:
from collections import defaultdict
names = ['julia', 'mark', 'thomas', 'rachel', 'alex', 'maria']
d = defaultdict(list)
for name in names:
key = len(name)
d[key].append(name)
print(dict(d)) # prints {5: ['julia', 'maria'], 4: ['mark', 'alex'], 6: ['thomas', 'rachel']}
All you have to do to group based on some other function is change the key
to something else.
p = 'alex', 'blue', 20, 'c' # same as p = ('alex', 'blue', 20, 'c')
name, color, age, lang = p
print(p) # prints a tuple - ('alex', 'blue', 20, 'c')
print(name, color, age, lang) # prints alex blue 20 c
Note: In the same manner, swapping 2 variables in python might be the most elegant way out of all the languages:
x, y = 1, 2
# swap x and y
x, y = y, x
print(x, y) # prints 2 1
Use the join()
method to concatenate strings from an iterable.
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
# join the strings from langs, having ', ' as separator
text = ', '.join(langs)
print(text) # prints c, python, java, c++, kotlin, rust
Most (!not all) of the builtin data types methods are implemented using C function calls, so that makes it atomic.
For a better explanaton check here.
Also, dictionaries' popitem()
is atomic, while pop()
may not, based on the key type (if the key is not a builtin data type, Python has to call that object's __hash__()
implementation), so better use popitem()
where atomicity is needed.
d = {
'foo': 'c',
'bar': 'java',
'baz': 'rust'
}
while d:
key, value = d.popitem()
print(f'{key} --> {value}')
# prints
# foo --> c
# bar --> java
# baz --> rust
# d is empty at the end
Note: If unsure, don't hesitate to use mutexes!
When you have a dictionary that has some default values and you want to override it with another dictionary, use ChainMap()
. ChainMap()
has the advantage that it doesn't copy anything, it just "links" the dictionaries, using the initial memory (this also means that any change in the initial dictionary will be reflected in the ChainMap()
as well).
from collections import ChainMap
defaults = {
'bar': 'c',
'foo': 'java'
}
overwritten = {
'foo': 'rust',
'barn': 'c++'
}
d = ChainMap(overwritten, defaults)
print(dict(d)) # prints {'foo': 'rust', 'barn': 'c++', 'bar': 'c'}
Note: Don't use copy()
and then update()
, it is really bad performance-wise and can be replaced in 99% of the cases by a ChainMap()
.
d1 = {
'bar': 'c',
'foo': 'java'
}
d2 = {
'foo': 'rust',
'barn': 'c++'
}
# Don't do this!!
d = d1.copy()
d.update(d2)
Note 2: For a better example when this is useful, see this.
A dictionary is not guaranteed to preserve the order of insertion. It actually optimizes keys for faster lookup. However there is one way to have a dictionary preserve insertion order, using OrderedDict()
from collections
.
from collections import OrderedDict
d = OrderedDict()
d['bar'] = 'c'
d['foo'] = 'java'
d['baz'] = 'rust'
print(dict(d)) # prints {'bar': 'c', 'foo': 'java', 'baz': 'rust'}
Note: Since Python 3.7, regular dict
s have guaranteed ordering. More here. Note however that they don't completely replace OrderedDict
s, since they have extra features:
from collections import OrderedDict
a = {1: 1, 2: 2}
b = {2: 2, 1: 1}
c = OrderedDict(a)
d = OrderedDict(b)
print(a == b) # returns True
print(c == d) # returns False since OrderedDicts are order-sensitive, and regular dicts are not
Also, OrderedDict
s have methods to change order of elements, while regular dict
s don't.
Deques (double ended queues) are really fast in python3. They are implemented using doubly-linked lists, so inserting and removing at the end or at the beginning is O(1) complexity. Lists are implemented as normal arrays, so they have to sometimes realloc()
to accomodate for the number of elements (only sometimes because by default it realloc()
s more memory at the time than necessary'), so that makes them have O(n) complexity when inserting or removing at the beginning because they have to copy the rest of the elements.
Generally, updating a sequence is MUCH faster when using a deque()
as opposed to using a list()
(though keep in mind that accessing a random element in a deque()
is expensive, whereas accessing a random element in a list()
is O(1)).
from collections import deque
# Wrong!
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
del langs[0]
langs.pop(0)
langs.insert(0, 'scala')
# Right!
langs = deque(['c', 'python', 'java', 'c++', 'kotlin', 'rust'])
del langs[0]
langs.popleft(0)
langs.appendleft(0, 'scala')
Usually there is the case that code like this is written in other languages:
from decimal import getcontext, setcontext
old_context = getcontext().copy()
getcontext().prec = 50
print(Decimal(355) / Decimal(113))
setcontext(old_context)
This can easily be replaced with contexts:
from decimal import localcontext, Context
with localcontext(Context(prec=50)):
print(Decimal(355) / Decimal(113))
Other examples:
- Writing or reading from file
f = open('data.txt')
try:
data = f.read()
# do something with data
finally:
f.close()
can be replaced with:
with open('data.txt') as f:
data = f.read()
# do something with data
- Deleting a file (getting rid of the try-except-pass idiom):
try:
os.remove('sometempfile.tmp')
except OSError:
pass
can be replaced with:
from contextlib import suppress
with suppress(FileNotFoundError):
os.remove('sometempfile.tmp')
Note: suppress()
is a reentrant context manager. More info here.
- Using a lock
lock = threading.Lock()
lock.acquire()
try:
# critical section
finally:
lock.release()
can be replaced with:
lock = threading.Lock()
with lock:
# critical section
Note: For reentrant lock context manager, see threading.RLock.
- Redirecting output from stdout to file
with open('help.txt', 'w') as f:
sldstdout = sys.stdout
sys.stdout = f
try:
help(pow)
finally:
sys.stdout = oldstdout
can be replaced with:
with open('help.txt', 'w') as f:
with redirect_stdout(f):
help(pow)
Note: redirect_stdout()
is also a reentrant context manager.
More on context managers here.
For example, looking up a webpage numerous times is expensive, and usually the result is the same. So use the lru_cache()
decorator:
from functools import lru_cache
@lru_cache
def web_lookup(url):
return urllib.urlopen(url).read()
More can be found here.
Using the any()
function, you can check if at least one value in the iterable is True
. It applies the bool()
function to every element.
false_lst = [0, False, '', 0.0, [], {}, None] # all of these return False when using bool() on them
print(any(false_lst)) # prints False
true_lst = [1, True, 'x', 3.14, ['x'], {'a': 'b'}] # all of these return True when using bool() on them
print(any(true_lst)) # prints True
falst_lst.append(-1) # any integer different from 0 is considered True
print(any(false_lst)) # prints True
Note: This function shortcircuits, meaning the first time it finds True
it returns; it does NOT check for the rest of the values to be True
.
Note 2: It is really useful with generators:
print(any(range(1000000)) # prints True after 2 values evaluated, as range() is a generator
print(any([range(1000000)])) # prints True after the whole list of 1000000 elements has been initialized, as range() has to populate the list first
There is another function, all()
, that does what it says: it tests for all the elements in the sequence to be True
, and works much in the same way as any()
.
Aside from the fact that namedtuple()
s are more verbose, they also offer better usage, as they can be treated as regular tuples, classes or even dictionaries.
For example, having a point:
pt1 = (2, 3)
print(pt1[0], pt1[1]) # prints 2 3
can be replaced with the better alternative namedtuple()
:
from collections import namedtuple
Point = namedtuple('Point', 'x y') # a tuple named 'Point' with attributes 'x' and 'y'
# alternatively this means the exact same thing
# Point = namedtuple('Point', ['x', 'y'])
pt1 = Point(2, 3)
print(pt1) # prints Point(x=2, y=3)
print(pt1.x, pt1.y) # prints 2 3
print(pt1[0], pt1[1]) # prints 2 3
print(dict(pt1._asdict())) # prints {'x': 2, 'y': 3}
print(pt1._replace(x=50)) # prints Point(x=50, y=3)
# Note however that _replace() returns a modified copy. The original is still a tuple, so it cannot be modified
Another common example:
from collections import namedtuple
Person = namedtuple('Person', 'age color lang')
person = Person(31, 'blue', 'c')
print(person) # prints Person(age=31, color='blue', lang='c')
Note: When the values from a namedtuple()
are invalid (e.g. having one of the fields named class
or having the same field twice), it throws a ValueError
. To avoid this you can possibly provide a third parameter named rename
. If set to True
, it will rename the field that is incorrect.
from collections import namedtuple
Person = namedtuple('Person', 'age color age', rename=True)
print(Person(31, 'blue', 'whatever')) # prints Person(age=31, color='blue', _2='whatever')
Note 2: Since Python 3.8, _asdict()
method returns a regular dictionary, as regular dict
s now have guaranteed ordering based on insertion (since Python 3.7).
Since Python 3.8, cProfile can be used as a context manager, making it extremely easy to profile code.
import cProfile
with cProfile.Profile() as profiler:
# code to be profiled
profiler.print_stats()
One way to format the output is to use the pprint
module.
from pprint import pprint
d = {
'b': [*range(5)],
'c': []
'a': "Here is a long string".split(" "),
}
pprint(d, indent=2, width=20, compact=True)
# prints
# { 'a': [ 'Here',
# 'is', 'a',
# 'long',
# 'string'],
# 'b': [ 0, 1, 2, 3,
# 4],
# 'c': []}
Note: Since Python 3.8, the parameter sort_dicts
was added (True
by default):
from pprint import pprint
d = {
'b': [*range(5)],
'c': []
'a': "Here is a long string".split(" "),
}
pprint(d, indent=2, width=20, compact=True, sort_dicts=False)
# prints
# { 'b': [ 0, 1, 2, 3,
# 4],
# 'c': [],
# 'a': [ 'Here',
# 'is', 'a',
# 'long',
# 'string']}
More info here.
The is
operator checks if 2 objects point to the same memory address. The equality operator ==
checks if 2 objects are equal.
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
copy = langs # now copy and langs point to the same memory object
print(copy == langs) # prints True
print(copy is langs) # prints True
other_copy = list(langs) # other_copy has a copy of langs, but point to different memory objects
print(other_copy == langs) # prints True
print(other_copy is langs) # prints False
You can use slices to replace elements, delete elements or make a copy of a list.
- Delete items:
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
del langs[0:3]
print(langs) # prints ['c++', 'kotlin', 'rust']
- Replace elements of a list without creating a new list object
langs = ['c', 'python', 'java', 'c++', 'kotlin', 'rust']
copy = langs
print(copy is langs) # prints True
langs[:] = [41, 42, 43]
print(copy is langs) # prints True
print(copy) # prints [41, 42, 43]
langs = [1, 2, 3]
print(copy is langs) # prints False, langs points to new list (new memory object)
- Make a (shallow) copy of a list
langs = ['c', ['python', 'java'], 'c++', 'kotlin', 'rust']
copy = langs[:]
copy[1][0] = 'some other lang'
print(langs) # prints ['c', ['some other lang', 'java'], 'c++', 'kotlin', 'rust']
Note: If you need a deep copy consider using the function deepcopy()
from the module copy
.
There are 2 types of copies in Python. One is the shallow copy, that works very similar to how assigning to pointers works in C (they only reference the object they point to, changing one also changes the other), and the other is the deep copy, which makes a perfect copy of the object.
import copy
list1 = [1, 2, [3, 4], 5]
list2 = copy.copy(list1)
list2[2][1] = 6
list2[0] = 7
# shallow copy, list2 holds references to objects in list1, changing one also changes the other
print(list1) # prints [1, 2, [3, 6], 5]
print(list2) # prints [7, 2, [3, 6], 5]
list3 = copy.deepcopy(list1)
list3[2][1] = 8
list3[0] = 9
# deep copy, list3 is a perfect copy of list1 with no references to it, changing one doesn't change the other
print(list1) # prints [1, 2, [3, 6], 5]
print(list3) # prints [9, 2, [3, 8], 5]
More about deep and shallow copies here.
Python has a built-in http server; it can be super useful if you want to preview a website without going the hurdle of starting an apache or nginx server.
This serves the website in the current directory at address localhost:8000
:
python3 -m http.server
Python 3.5 supports type annotations, which can ensure better readability. Note however that they are only there for the programmer to acknowledge, Python does not care and won't change anything based on them.
def func(s1: int, s2: int = 42) -> int:
return s1 + s2
They can be changed to anything you want:
def func2(page: 'web page', request: 'web request') -> 'web response':
# return response
Note: Passing 2 strings to func()
is perfectly valid, as Python does NOT care at all about these annotations (in this case the function would return the 2 strings concatenated).
Note 2: You can use stuff like Mypy to enforce this kind of behaviour, so Python becomes statically-typed!
More info about type annotations can be found in PEP 484.
Note 3: Since Python 3.6, PEP 526, more support for type annotations was added. Again, Python will always be a dynamically-typed language, but tools can be used to ensure static typing.
This is an easy method to find the most common elements in an iterable:
import collections
count = collections.Counter('some random string')
print(c.most_common())
# prints [('s', 2), ('o', 2), ('m', 2), (' ', 2), ('r', 2), ('n', 2), ('e', 1), ('a', 1), ('d', 1), ('t', 1), ('i', 1), ('g', 1)]
print(c.most_common(3))
# prints [('s', 2), ('o', 2), ('m', 2)]
More info can be found in the Python docs for the Counter class.
Get permutations of an iterable:
import itertools
lst = list(itertools.permutations('abc', 2))
print(lst)
# prints [('a', 'b'), ('a', 'c'), ('b', 'a'), ('b', 'c'), ('c', 'a'), ('c', 'b')]
The function takes an iterable and another optional argument specifying the length of one permutation.
Python has 2 methods to transform an object into a string (similar to other languages toString()
methods); those are str()
and repr()
.
import datetime
now = datetime.date.today()
print(str(now)) # prints '2020-02-12'
print(repr(now)) # prints 'datetime.date(2020, 2, 12)'
The function str()
is made for clarity, while the function repr()
is made to be unambiguos about what the object represents.
The python console uses repr()
.
Python has a built-in disassembler. It is very rudimentary, but it can help debug some code.
import dis
def func(text):
return 'This is some text \'' + str(text) + '\'.'
dis.dis(func)
# 4 0 LOAD_CONST 1 ("This is some text '")
# 2 LOAD_GLOBAL 0 (str)
# 4 LOAD_FAST 0 (text)
# 6 CALL_FUNCTION 1
# 8 BINARY_ADD
# 10 LOAD_CONST 2 ("'.")
# 12 BINARY_ADD
# 14 RETURN_VALUE
More info in the docs.
Lambda functions, as in other functional programming languages, are anonymous functions that don't have a name. They are useful for small code that doesn't require more than a line or two, and they are generally passed as arguments to other functions.
One such example that applies to all functional programming languages is the map()
function. It takes a callable as the first argument (read function, lambda function, something that can be called), and an iterable as the second argument, and applies the function to each of the elements of the iterable, returning a new iterable.
obj = map(lambda string: string.lower(), ['StRiNg', 'ANOTHER string'])
print(list(obj))
# prints ['string', 'another string']
This code does the exact same thing:
def stringlower(string):
return string.lower()
lst = ['StRiNg', 'ANOTHER string']
obj = []
for item in lst:
obj.append(stringlower(item))
print(obj)
# prints ['string', 'another string']
Another example:
power_func = lambda x, y: x ** y
print(power_func(2, 3)) # prints 8
x = (lambda a, b: a - b)(5, 4)
print(x) # prints 1
Python has an interesting module to work with Ip addresses:
import ipaddress
address = ipaddress.ip_address('192.168.100.14')
print(repr(address))
# prints IPv4Address('192.168.100.14')
# you can even have arithmetic operations done on this address
print(address + 3)
# prints 192.168.100.17
More info here.
In Python, you can check if a class is a subclass of some other class:
class BaseClass(): pass
class SubClass(BaseClass): pass
print(issubclass(SubClass, BaseClass)) # prints True
print(issubclass(SubClass, object)) # prints True
You can also check if some instance is an instance of the specified class or another sublass of that class:
class BaseClass(): pass
class SubClass(BaseClass): pass
obj = SubClass()
print(isinstance(obj, BaseClass)) # prints True
41. Asterisk (*) and slash (\) in function definition (positional- and keyword-only function parameters)
In Python 3, you can add an asterisk and a slash to a function definition with special meaning. Asterisk marks keyword-only parameters (that means parameters that can be given to the function just by keyword, not by position), while slash marks positional-only parameters (meaning parameters cannot be given by keyword, but by position only).
def func(positional_only_argument, /, positional_and_keyword_argument, *, keyword_only_argument):
return positional_only_argument + positional_and_keyword_argument + keyword_only_argument
print(func(1, 2, 3))
# Type error, third parameter should be keyword
print(func(positional_only_argument = 1, 2, 3))
# Type error, first parameter is positional only
print(func(1, 2, keyword_only_argument = 3))
# fine, prints 6
print(func(1, positional_and_keyword_argument = 2, keyword_only_argument = 3))
# fine, prints 6
Info and rationale about these 2 types of parameters can be found in PEP 3102 - keyword-only parameters and in PEP 570 - positional-only parameters.
Note: Until Python 3.8, positional-only arguments could only be used in library functions. Starting from Python 3.8, they can be used in programmer constructions too.
Say you wrote some Python code like this:
def min(a, b):
return a if a < b else b
You can launch it in an interactive shell with python -i main.py
, which is similar to calling only python
in the command line, with the key difference that the python shell contains your function in the global scope as well. Go ahead, try it!
Python has a debugger, similar to gdb. One way to use it is to simply add import pdb; pdb.set_trace()
in your program wherever u want the debugger to stop program execution.
In Python 3.7, the debugger can also be called on a script like this: python -m pdb script.py
, and it stops when the module loads, just before executing the first line of the script.
def add(a, b):
return a + b
import pdb
pdb.set_trace()
# code execution will stop here, and the program will enter the debugger
print(add(1, 2))
For more information on how to operate the python debugger, visit this.
Note: Since Python 3.7, instead of import pdb; pdb.set_trace()
, you can simply add a breakpoint()
function call whenever you want the program to stop execution.
Python 3.8 introduced assignment expressions through the use of a new operator, called the walrus operator (if you look sideways, the operator looks like a walrus).
Assignment expressions allow you to assign and return a value in the same expression, similar to how things work in a language like C.
while (x := int(input("What is your age?"))) > 18:
print("You are a grown-up!")
else:
print("You are a kid!")
It can be useful, for example in list comprehensions:
lst = [y for x in 'abcd' if (y := f(x)) is not None]
# instead of having to compute f(x) twice
lst = [f(x) for x in 'abcd' if f(x) is not None]
Arguably, the operator is a little confusing, and most of the times not needed and can be replaced with more expressive syntax. There are good arguments to why this operator is not needed in Python here.
Nonetheless, Python 3.8 adopted assignment expressions through the use of the walrus operator :=.
For more info on the walrus operator and assignment expressions, see PEP 572.
Formatted string literals (or f-strings) are a construct added in Python 3.6 and have since become very popular due to the speed (see tip 7) and simplicity.
Some examples:
number = 3.1415
width = 10
precision = 3
print(f'This is {number:{width}.{precision}}')
# prints
# This is 3.14
There are three conversion fields; r
, s
and a
. What they do is call the functions repr()
, str()
and ascii()
respectively on the formatted parameter.
name = 'Alex'
print(f'My name is {name!r}')
# prints
# My name is 'Alex'
Since Python 3.8, there is a new specifier (=), that expands to the representation of the expression, making it useful for debugging and self-documenting.
import datetime
name = 'Alex'
print(f'{name=}') # prints name='Alex'
now = datetime.date.today()
print(f'{now=}') # prints now=datetime.date(2020, 2, 14)
# f-string specifiers still work
print(f'{now=!s}') # prints now=2020-02-14
number = 3.1415
# Careful when adding format specifiers
print(f'{number + 1=:10.2f}') # prints number + 1= 4.14
print(f'{number + 1=:10.2}') # prints number + 1= 4.1
More info about f-strings in the docs.
Note: Formatted strings have a 'formatting' option similar to how printf()
works in other languages. Python's implementation of formatted print is a little more advanced though.
Say you have a function:
def sum(a, b):
"""This function adds 2 numbers and returns the result."""
return a + b
But now we want to log this function call. Of course, adding this code in the implementation of the function is bad, since we're polluting the function code. Even more so, what if we want to log another 10 function calls?
For this purpose, we can easily use a decorator.
def log(func):
def wrapper(*args, **kwargs):
"""Wrapper function."""
# do some logging
return func(*args, **kwargs)
return wrapper
Now it is easy to use the decorator on whatever function we want to log.
@log
def sum(a, b):
"""This function adds 2 numbers and returns the result."""
return a + b
print(sum(4, 5)) # this function call will be logged
However, one problem arises when decorating a function like this. If we now try to get the doc or the function name, we notice that we get the information of the wrapper function, rather than that of our initial function:
print(sum.__doc__) # prints "Wrapper function"
print(sum.__name__) # prints "wrapper"
This is not ideal, considering that debuggers and other introspection tools use this. To fix this, we can use functools.wraps.
import functools
def log(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
"""Wrapper function."""
# do some logging
return func(*args, **kwargs)
return wrapper
@log
def sum(a, b):
"""This function adds 2 numbers and returns the result."""
return a + b
print(sum.__doc__) # prints "This function adds 2 numbers and returns the result."
print(sum.__name__) # prints "sum"
Python does not have a built-in method to have a static variable in a function like C or other languages do through the use of the static
keyword.
Instead, we can use the fact that functions are first-class objects in Python and we can assign variables to them.
def func():
try:
func.number_of_times_called += 1
except:
func.number_of_times_called = 1
# some really interesting code
This is better than having a global variable pollute the global namespace, and is better than having a decorator that does that (because the decorator runs when the python module is loaded even if the function might never be called, so the decorator will still do some work and initialize some value; instead here the code runs only when the function is called, if ever).