Skip to content

Commit 1119dc8

Browse files
committed
post: python 3 talk tba
1 parent 7461c6c commit 1119dc8

File tree

1 file changed

+273
-0
lines changed

1 file changed

+273
-0
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
---
2+
layout: post
3+
title: "Python 3: pitfalls, tips and tricks"
4+
date: 2020-08-27 13:37:11 +0200
5+
categories: python talk draft
6+
---
7+
8+
I wanted to write a post about some common things I see when reading
9+
and writing Python scripts. I'm also including some thoughts on how to keep
10+
the code clean and readable, and making the most of the tools already built
11+
into the standard Python library.
12+
13+
This post will be continously updated as time passes by. I will eventually
14+
present it as a talk.
15+
16+
## String Interpolation
17+
18+
We have some different methods to choose from when it comes to string
19+
interpolation. Some times it might be nice enough to just concatenate:
20+
21+
### Concatenating Strings with Variables
22+
23+
```python
24+
def html_list(strings):
25+
"""Takes a list of strings and returns a HTML unordered list"""
26+
html = "<ul>"
27+
for s in strings:
28+
html += "<li>" + s + "</li>"
29+
html += "</ul>"
30+
return html
31+
32+
print(html_list(["light", "sun", "moon", "water"]))
33+
#eval
34+
```
35+
36+
but when more than two strings are concatenated with variables, it tends
37+
to get messy, hence, decreased readability.
38+
39+
Building an HTML string, like in the example above, but with multiple
40+
interpolation, will quickly get messy if we need to do it a lot. Creating
41+
a string containing a HTML element with multiple properties might look
42+
like something like this:
43+
44+
```python
45+
params = dict(color="#FF9900", value="Orange", hover="A fruit that tastes good")
46+
html = '<span style="display: inline-block; padding: .5em; background-color: ' \
47+
+ params["color"] + ';" title="' + params["hover"] + '">' \
48+
+ params["value"] + '</span>'
49+
print(html)
50+
#eval
51+
```
52+
53+
Since this is a fairly long string, and we already have the params stored
54+
in a `dict`, it's a perfect suit for `str.format()`.
55+
56+
### str.format
57+
58+
This is a good choice when you want to use the same argument more than once in
59+
a single line, or when you want to apply special formatting to the variables.
60+
61+
#### Interpolating values in a `dict`
62+
63+
The same example as in plain concatenation, but now using `str.format`. This
64+
function accepts both positional and keyword arguments, which lets us take
65+
advantage of dictionary item *unpacking*, using the `**` syntax.
66+
67+
```python
68+
params = dict(color="#FF9900", value="Orange", hover="A fruit that tastes good")
69+
html = '<span style="display: inline-block; padding: .5em;' \
70+
'background-color: {color};" title="{hover}">{value}</span>'
71+
print(html.format(**params))
72+
#eval
73+
```
74+
75+
You can see how this immediately makes this easier to follow by removing
76+
the clutter with open/close quotes and `+` signs.
77+
78+
Other great uses for `str.format()` not covered here:
79+
- binary and hex formatting
80+
- prefixing and suffixing numerical types
81+
- text alignment, indentation and positioning
82+
83+
### f-strings
84+
85+
The *f-strings* comes in handy when writing strings containing multiple
86+
variables of basic types. With basic types, I mean types link `str` and
87+
`int` that hold simple values, in contrast to a `dict` where we will have
88+
to reference the items using the `["key"]` syntax.
89+
90+
```python
91+
thing = "luna"
92+
nickname = "moon"
93+
print(f"There is a {thing} that looks really nice. It's called {nickname}")
94+
#eval
95+
```
96+
97+
I think it makes more sense to use f-strings when writing long, multilined
98+
strings, as you can easily read what variables will be filled in where,
99+
without having to read the arguments passed to e.g. `str.format()`.
100+
They act very similar, but we dont' have to call the function to format
101+
the string, which is a nice bonus.
102+
103+
An example function to create a systemd service file:
104+
105+
```python
106+
def systemd_service(*, binary, description=""):
107+
return f'''
108+
[Unit]
109+
Description={description}
110+
111+
[Service]
112+
Type=simple
113+
ExecStart={binary}
114+
Restart=always
115+
RestartSec=10
116+
'''
117+
118+
svc = systemd_service(binary="/bin/nc -lkuv 127.0.0.1 3000",
119+
description="Listen for UDP connections on port 3000")
120+
print(svc)
121+
#eval
122+
```
123+
124+
### % (percent) formatting
125+
126+
Very good to keep syntax clean.
127+
128+
#### Pitfalls
129+
130+
The syntax actually expects a tuple after the `%`, so the following will fail
131+
132+
```python
133+
s = [1, 2, 3]
134+
```
135+
136+
## Cheap Type Safety
137+
138+
### Type assertions
139+
140+
I am not leaning on type safety when writing Python, but some times I still
141+
want to have explicit type checks to keep my head clean at write-time, while
142+
at the same time improving the readability of the function body.
143+
144+
```python
145+
def generate_cert(*, name, hostnames):
146+
assert isinstance(name, str), "name should be a string"
147+
assert isinstance(hostnames, list), "hostnames should be a list"
148+
149+
domains = ",".join(hostnames)
150+
cmd = f"certbot certonly --domains {domains} --cert-name {hostnames[0]}"
151+
return subprocess.check_output(cmd)
152+
```
153+
154+
This immediately helps understand what the expected types of the arguments
155+
are, while at the same time throwing `AssertionError`s if the assertions
156+
fail. This can save you time and potential headaches while writing or debugging.
157+
158+
**Warning:** `assert` statements are ignored when `__debug__ == False`,
159+
i.e. when Python is started with optimisations enabled (`python -O`).
160+
161+
### Exceptions
162+
163+
If you are simply re-throwing the exception you've caught, you don't have to
164+
assign it to a variable if you're not going to process it.
165+
166+
Instead of the following:
167+
168+
```python
169+
try:
170+
throwing_code("some value")
171+
except ValueError as e:
172+
graceful_cleanup()
173+
raise e
174+
```
175+
176+
We can instead re-throw the caught exception in the exception handler implicitly by
177+
not passing any arguments to `raise`.
178+
179+
```python
180+
try:
181+
throwing_code("some value")
182+
except ValueError:
183+
graceful_cleanup()
184+
raise
185+
```
186+
187+
See the logging section for neat ways to log exceptions.
188+
189+
## Logging
190+
### String interpolation
191+
192+
Something I often see is using `str.format(...)` or `"%s:%s" % (a, b)`
193+
expressions when using the `logging` library.
194+
195+
```python
196+
import logging
197+
198+
name = "world"
199+
age = 42
200+
logging.error("hello {0:s}, I am {1:d} years old".format(name, age))
201+
#eval
202+
```
203+
204+
However, string formatting is already built into `logging.*` functions,
205+
but without the positional syntax. Very often we don't need to log the
206+
same variable more than once in a single log line, so we can get away
207+
with using the supported `%<type>` syntax.
208+
209+
```python
210+
import logging
211+
212+
name = "world"
213+
age = 42
214+
logging.error("hello %s, I am %d years old", name, age)
215+
#eval
216+
```
217+
218+
### Exceptions
219+
220+
In the following lines we are catching the exception and assigning it to `e`.
221+
222+
```python
223+
import logging
224+
225+
def generate_cert(domain):
226+
raise ValueError("invalid domain: %s" % domain)
227+
228+
for host in ["blog.stigok.com", "www.stigok.com"]:
229+
logging.info("Generating cert(s) for %s", host)
230+
try:
231+
output = generate_cert(host)
232+
except ValueError as e:
233+
logging.error("Failed to generate cert:")
234+
logging.error(e)
235+
break
236+
#eval
237+
```
238+
239+
We don't actually have to assign the exception to `e` if we only want to print it.
240+
`logging.exception()` lets us write an error message and automatically adds the
241+
exception message to the log line.
242+
243+
```python
244+
import logging
245+
246+
def generate_cert(domain):
247+
raise ValueError("invalid domain: %s" % domain)
248+
249+
for host in ["blog.stigok.com", "www.stigok.com"]:
250+
logging.info("Generating cert(s) for %s", host)
251+
try:
252+
output = generate_cert(host)
253+
except ValueError:
254+
logging.exception("Failed to generate certs")
255+
break
256+
#eval
257+
```
258+
259+
This gives us clean code and a clean error message -- with a stack trace,
260+
and we don't assign the `ValueError` to a variable.
261+
262+
Note that `logging.exception()` should only be called inside an `except`
263+
block (exception handler), and that we should not pass the exception itself
264+
as an argument.
265+
266+
```python
267+
# Don't do this
268+
except subprocess.CalledProcessError as e:
269+
logging.exception("Failed to generate certs", e)
270+
```
271+
272+
## References
273+
- https://docs.python.org/3/library/

0 commit comments

Comments
 (0)