Last week I had the opportunity to see three interesting questions discussed in the Telegram groups, but the explanation would be too long for a chat.

  1. Why True, True, True == (True, True, True) returns True, True, False?

  2. Why -2 * 5 // 3 + 1 returns -3?

  3. How to add times in Python?

Why True, True, True == (True, True, True) returns True, True, False?#

This question was presented as a weird syntax of Python, but it’s actually a visual trap. Look at the operator == (equal equal).

Python 3.8.2 (default, Apr 27 2020, 15:53:34) 
[GCC 9.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> True, True, True == (True, True, True)
(True, True, False)

Some people were in doubt and asked why (True, True, False) instead of (True, True, True). Firstly, we should remember that Python allows us to create tuples without using parentheses, only with commas. And it’s exactly this syntax that caused some confusion, because looking quickly you might think we’re comparing two tuples, which is not the case. For example:

>>> (True, True, True) == (True, True, True)
True

The difference are the parentheses. Without parentheses, we’re only comparing True == (True, True, True) and not the first tuple with the second. It’s a matter of operator priority. So, to create the tuple, the interpreter evaluates the comparison of True with (True, True, True) first. Since the first is of type bool and the second is a tuple, the result is False. Thus, the generated tuple has the first two Trues followed by the False that’s the result of the comparison. When we write between parentheses, we’re comparing two tuples and the result is True.

Why -2 * 5 // 3 + 1 returns -3?#

This one is more complicated. Several results were presented and the priority of the operator // was questioned. Let’s see what Python says:

>>> -2 * 5 // 3 + 1
-3

What result did you expect? Some people expected -4, others -2. How does it result in -3?

Firstly, we should review the priority of the operator // which is exactly the same as division. In this case, the division and multiplication have the same priority and must be evaluated from left to right. This is especially important because // performs a rounding down.

Now let’s see the definition of // in the Python documentation:

Division of integers yields a float, while floor division of integers results in an integer; the result is that of mathematical division with the ‘floor’ function applied to the result…

Which we can translate as: Division of integers results in a floating-point number (float), while floor division of integers results in an integer (int); the result is that of mathematical division with the application of the floor function applied to the result.

The point that was not clear is the behavior of floor with negative numbers.

>>> 10 // 3
3
>>> -10 // 3
-4
>>> -10 / 3
-3.3333333333333335

Naturally, we would expect the result of -10 // 3 to be equal to that of 10 // 3, but with a negative sign. You can check the definition of these two functions in Wikipedia and the Python documentation:

math.floor(x) Return the floor of x, the largest integer less than or equal to x. If x is not a float, delegates to x.__floor__(), which should return an Integral value

Which can be translated as: returns the floor of x, the largest integer less than or equal to x. If x is not a floating-point number, delegates to x.__floor__() which should return an integral value.

For negative numbers, the part of returning the smallest integer misleads easily. Because the smallest integer relative to -3.33 is -4, keeping in mind that `-3 > -4!

So the expression is correctly evaluated by the interpreter:

-2 * 5
-10
-10 // 3
-4
-4 + 1
-3

Looking at the sources of Python (3.8.2) on GitHub, it’s easy to see the adjustment (line 3768):

3751/* Fast floor division for single-digit longs. */
3752static PyObject *
3753fast_floor_div(PyLongObject *a, PyLongObject *b)
3754{
3755    sdigit left = a->ob_digit[0];
3756    sdigit right = b->ob_digit[0];
3757    sdigit div;
3758
3759    assert(Py_ABS(Py_SIZE(a)) == 1);
3760    assert(Py_ABS(Py_SIZE(b)) == 1);
3761
3762    if (Py_SIZE(a) == Py_SIZE(b)) {
3763        /* 'a' and 'b' have the same sign. */
3764        div = left / right;
3765    }
3766    else {
3767        /* Either 'a' or 'b' is negative. */      
3768        div = -1 - (left - 1) / right;              
3769    }
3770
3771    return PyLong_FromLong(div);
3772}

Note: I recall calling the floor function of sole or minimum.

How to add times in Python?#

A colleague from the group asked how to add time durations in Python. Presenting the following code:

from datetime import time
t0 = time.fromisoformat('06:52:00')
t1 = time.fromisoformat('00:08:15') 
t2 = time.fromisoformat('00:07:12') 
t3 = t0 + t1 + t2

which results in the following error:

Traceback (most recent call last):
  File "tdeltao.py", line 5, in <module>
    t3 = t0 + t1 + t2
TypeError: unsupported operand type(s) for +: 'datetime.time' and 'datetime.time'

This happens because the time class does not define the addition operation. The correct class for this type of calculation is timedelta.

To correctly calculate this sum, we need to first convert the string to a timedelta object. This can be done with a simple function:

from datetime import time, timedelta, datetime

def string_para_timedelta(str_time: str) -> timedelta:
    valor = time.fromisoformat(str_time)
    return timedelta(hours=valor.hour, 
                     minutes=valor.minute, 
                     seconds=valor.second)

t0 = string_para_timedelta('06:52:00')
t1 = string_para_timedelta('00:08:15') 
t2 = string_para_timedelta('00:07:12') 
t3 = t0 + t1 + t2
print(t3)
print(datetime.now() + t3)

which results in:

7:07:27
2020-05-03 22:48:55.473647

A tip to remember the difference between time and timedelta is that time cannot represent more than 24 hours and timedelta can represent more than a century (270 years). Another advantage of timedelta is that it can be used in operations with date and datetime.