The curious case of the else in Python loops

One of the first things to stand out when I was starting with Python was the else clause. I guess everyone knows the normal usage of such clauses in any programming language, which is to define an alternate path for the if condition. Oddly enough, in Python we can add else clauses in loop constructions, such as for and while. For example, this is valid Python:

for number in some_sequence:
    if is_the_magic_number(number):
        print('found the magic number')
        break
else:
    print('magic number not found')

Notice how the else is aligned with the for and not with the if. What this means is that commands inside the else block will be executed if, and only if, the loop was not finished by a break. The same is true for while loops.

I must admit that I’ve always had some trouble to remember the meaning of an else in loops, specially because I don’t see them very often (and I’m grateful for that). But, at some day I was watching Raymond Hettinger’s Transforming Code into Beautiful, Idiomatic Python talk where he brilliantly says something like this at some point:

Why don’t you call the else in loops as ‘nobreak’?

That’s all I needed to not forget the meaning anymore. ๐Ÿ™‚

How to customize your IPython 5+ prompt

IPython is wonderful and I โค๏ธ it. I can’t see myself using the default Python shell in a daily basis. However, its default prompt kind of annoys me:

Some things I dislike:

  • the banner displayed when we start it;
  • the In[x] and Out[x] displayed for inputs and outputs;
  • the newline in between commands;
  • and last, but far from least, the uber-annoyingย “do you really want to exit?”ย message.

As you can see, it doesn’t take much to get on my nerves.ย ๐Ÿ˜†

The bright side is that it’s easy to change that and have a more pleasant experience with IPython. This is my ideal shell, more compact and less bureaucratic:

 

If you like it, follow me through the next steps to make your IPython shell look and behave like that.

Customizing the prompt

Firstย you have to create a default profile for your shell with this command:

$ ipython profile create

As a result, a .ipython folder will be created in your home folder, with the following contents:

.ipython
โ”œโ”€โ”€ extensions
โ”œโ”€โ”€ nbextensions
โ””โ”€โ”€ profile_default
    โ”œโ”€โ”€ ipython_config.py
    โ”œโ”€โ”€ log
    โ”œโ”€โ”€ pid
    โ”œโ”€โ”€ security
    โ””โ”€โ”€ startup
        โ””โ”€โ”€ README

Next, createย ย .ipython/custom_prompt.pyย file with the following content:

from IPython.terminal.prompts import Prompts, Token


class CustomPrompt(Prompts):

    def in_prompt_tokens(self, cli=None):
        return [(Token.Prompt, '>>> '), ]

    def out_prompt_tokens(self, cli=None):
        return [(Token.Prompt, ''), ]

    def continuation_prompt_tokens(self, cli=None, width=None):
        return [(Token.Prompt, ''), ]

And last, you have to tell IPython to use this new class as your prompt and in addition to custom settings.

You can do so by adding this code toย .ipython/profile_default/ipython_config.py:

from custom_prompt import CustomPrompt


c = get_config()

c.TerminalInteractiveShell.prompts_class = CustomPrompt
c.TerminalInteractiveShell.separate_in = ''
c.TerminalInteractiveShell.confirm_exit = False
c.TerminalIPythonApp.display_banner = False

That’s it, now you have a prompt like the one I’ve shown earlier. I hope it improves your experience with IPython as it did for me.

If you want to learn how to do further customizations, check the official documentation.

Ah, did I mention that I love IPython? Huge kudos and thanks for the team behind it! ๐Ÿ‘

Python 3 rounding oddities

Rounding a decimal number with Python 3 is as simple as invoking the round() builtin:

>>> round(1.2)
1
>>> round(1.8)
2

We can also pass an extra parameter called ndigits, which defines the precision we want in the result. Such parameter defaults to 0, but we can pass anything:

>>> round(1.847, ndigits=2)
1.85
>>> round(1.847, ndigits=1)
1.8

And what happens when we want to round a number like 1.5? Will it round it up or down? Let’s check:

>>> round(1.5)
2

It seems that it rounds up. Let’s check some other numbers to confirm:

>>> round(2.5)
2

Uh, now it went down! Let’s check some more:

>>> round(3.5)
4
>>> round(4.5)
4
>>> round(5.5)
6

wut

Calm down, there’s an explanation for this. In Python 3, round() works like this:

Round to the closest number.
If there’s a tie, round to the closest even number.

Now it makes sense. If we check the examples above, we’ll see that the rounding was always made to the closest even number:

>>> round(3.5)
4
>>> round(4.5)
4
>>> round(5.5)
6

What about Python 2?

Python 2 is quite different. When there’s a tie, the rounding is always made upwards in case the numbers are positive:

>>> round(1.5)
2.0
>>> round(2.5)
3.0

And downwards, when the numbers are negative:

>>> round(-1.5)
-2.0
>>> round(-2.5)
-3.0

Why the hell does Python 3 changed it?

The goal is to take the bias out of the rounding operations.

Imagine a bank where all the roundings are done upwards. By the end of the day, the bank earning report will show a value that is higher than what the bank actually earned. That’s what happens on Python 2:

>>> # Python 2
>>> values = [1.5, 2.5, 3.5, 4.5]
>>> sum(values)
12.0
>>> sum(round(v) for v in values)
14.0

Using Python 3’s round(), the rounded values tend to be amortized, because half of them round upwards and half of them round downwards, given that half the numbers are even and the other half are odd. Check the same code, but now running on Python 3:

>>> # Python 3
>>> values = [1.5, 2.5, 3.5, 4.5]
>>> sum(values)
12.0
>>> sum(round(v) for v in values)
12

This is no Python 3’s inovation. In fact, this kind of rounding is quite old and even has a proper name: Bankers Rounding.

Drop duplicates in order

Let’s say you have a list containing all the URLs extracted from a web page and you want to get rid of duplicate URLs.

The most common way of achieving that might be building a set from that list, given that such operation automatically drops the duplicates. Something like:

>>> urls = [
    'http://api.example.com/b',
    'http://api.example.com/a',
    'http://api.example.com/c',
    'http://api.example.com/b'
]
>>> set(urls)
{'http://api.example.com/a',
 'http://api.example.com/b',
 'http://api.example.com/c'}

The problem is that we just lost the original order of the list.

A good way to maintain the original order of the elements after removing the duplicates is by using this trick with collections.OrderedDict:

>>> from collections import OrderedDict
>>> list(OrderedDict.fromkeys(urls).keys())
['http://api.example.com/b',
 'http://api.example.com/a',
 'http://api.example.com/c']

Cool, huh? Now let’s dig into details to understand what the code above does.

OrderedDict is like a traditional Python dict with a (not so) slight difference: OrderedDict keeps the elements’ insertion order internally. This way, when we iterate over such an object, it will return its elements in the order in which they’ve been inserted.

Now, let’s breakdown the operations to understand what’s going on:

>>> odict = OrderedDict.fromkeys(urls)

The fromkeys() method creates a dictionary using the values passed as its first parameters as the keys and the second parameter as its values (or None if we pass nothing, as we did).

As a result we get:

>>> odict
OrderedDict([('http://api.example.com/b', None),
             ('http://api.example.com/a', None),
             ('http://api.example.com/c', None)])

Now that we have a dictionary with the URLs as the keys, we can call the keys() method to get only a sequence containing the URLs:

>>> list(odict.keys())
['http://api.example.com/b',
 'http://api.example.com/a',
 'http://api.example.com/c']

Easy like that. ๐Ÿ™‚

If you enjoyed this tip, subscribe to the blog, because I’ll be posting more content in the upcoming weeks.