|
| 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