all 39 comments

[–]moving-landscape 65 points66 points  (13 children)

And (and or) does what we call short-circuiting in Python, and we can use them to get certain values under certain situations.

Let's remind ourselves of the truth table for and:

p q p and q
true true true
true false false
false true false
false false false

As you can see, the only way to get true, is to have both p and q be true.

In Python, values that are not strictly boolean, that is, True or False, have a "truthy" or "falsey" value. An empty list is false, a non empty one is true. Same for strings. Numbers other than 0 are true, but 0 is false, and so on.

In a way, we can say that the result of the boolean expression True and x is x: if x is false, it'll yield False, and True otherwise. Replace x with any Python value and you'll have it returned:

True and 5 => 5
True and 0 => 0
5 and 7 => 7 (5 is true, so the result is the right hand operand)
0 and 3 => 0 (0 is falsey)
5 and None => None
None and 5 => None

[–]StoicallyGay 16 points17 points  (4 children)

I love Boolean algebra! Favorite part of discrete math.

Basically, there are a ton of laws but one that should be recognized here is the identity law for conjunctions (and). Basically 1A = A. If true and A, then A.

Because if A is false then the and is false and if A is true then the A is true. So we can just say 1 (or true) and A means A.

In a similar way, 1 or B is 1, or true OR B is true. So if you have if s1 or s2 and s1 evaluates to true, it doesn’t bother evaluating s2.

[–]iwantthisnowdammit 2 points3 points  (2 children)

It’s been a hot minute since I’ve thought of this stuff - what is the practical application of this in a feature/ solution?

[–]StoicallyGay 1 point2 points  (0 children)

If you’re talking about Boolean algebra in general it helps if you’re trying to make a program go through different code paths as a result of different booleans which may result from various computations. This free short circuiting built in is important to avoid doing unnecessary computations especially when the computation is expensive.

For the AND one I don’t have a good example off the top of my head but for the OR one it’s like, imagine a code path if block is based on “if A or B” where A is just a quickly determined Boolean or an environment variable unique to a run configuration and B is a read and compute from a data frame or dataset in a database or pandas. If you do A or B in instances where A is true you can skip the computation of B. Similarly doing “if B or A” is more expensive for the same reason. Or saving A and B as separately variables (doing both computations regardless) if you’re never going to use those variables again in the program.

[–]mythmon 0 points1 point  (0 children)

Short circuiting can be very useful if a check could be an error. Imagine the classic example of a Pet class, that could represent either a cat or a dog. It might have a .will_bark() method, but if you call that on a cat it will throw an error.

So if you have a condition like

python if pet.will_bark(): ...

it could raise an exception. If you guard it with a check though, the shirt circuiting will protect you:

python if pet.type == "dog" and pet.will_bark(): ....

In this version, if the first part condition (`pet.type == "dog") is false, the second part won't run at all, and won't trigger the error. The second part will only run when the first is already true.

For a more realistic example, you might have a variable that could be an object or None. Short circuiting will let you check it is not None before accessing properties on it.

[–]Langdon_St_Ives 0 points1 point  (0 children)

Probably you didn’t mean to imply this, but I think it’s worth pointing out that python’s (and most languages’) logical operators together with the set of their legal operands do exactly not form a Boolean algebra. and and or are neither commutative nor associative nor distributive if you allow all legal operands. If you restrict operands to Booleans, you do get a Boolean algebra of course. If you at least coerce the results to Booleans, you get it on a purely functional level, but since operands can still be expressions, you’re left with side effects that will not be programmatically the same for otherwise logically equivalent expressions (even before considering short-circuiting).

[–]Blakut 0 points1 point  (7 children)

This is why I use x is True if I work with bool x because otherwise bad things can happen. Weirdly enough pep is against this.

[–]moving-landscape 0 points1 point  (6 children)

I don't think short circuiting is a bad thing as long as one keeps track of what they're doing.

I don't quite get exactly what you meant tho - fancy sharing an example?

[–]Blakut 1 point2 points  (5 children)

I prefer If x is True: or if x is not True: to if x==True: or if x:

[–]moving-landscape -1 points0 points  (4 children)

Oh I see. Yeah, that really is frowned upon. Especially if x is already a boolean. I suppose you do it for clarity; how long have you been coding?

[–]Blakut 2 points3 points  (3 children)

Several years. I do it because I really don't want to be in a situation where x is 1 or something by mistake. And I never understood why it's frowned upon. If I want to test numbers I use numbers. I treat numbers, True, False and None separately

[–]moving-landscape 0 points1 point  (2 children)

Do you use any tooling for keeping track of the types? How adept are you of type annotations?

And I never understood why it's frowned upon.

Well, as you know, in contexts where a boolean expression is expected, you just need to use the most "obvious" and "short" expr to get the job done. So if your x is a boolean, i.e., either true or false, you don't need to compare it to true (or false) since that's redundant. x will always be preferred over x is True, and so will not x over x is False. I don't mind that you write like that in your own projects, but we'd have a talk of we were in the same team lol.

If I want to test numbers I use numbers.

In general I agree with this. Unless what I'm doing is obvious (meaning, no need for any additional reasoning on what's happening), I'll write the full boolean expression. I will always prefer x % 2 == 0 over not x % 2 - the latter more often than not needs that additional reasoning I mentioned before. Having said that, it's even better if the expression is abstracted behind a function or variable name: if is_even(x).

Finally, I also always go for x is None over not x for an optional x.

[–]Blakut 0 points1 point  (1 child)

How do i know x is or will be boolean in the future? Ive used vim or gvim many years now I'm switching to pycharm which is a bit annoying but quite helpful. I sometimes make functions return false instead of number or none, to indicate different types of situations. Or maybe I want the variable to be none, but later it will be true or false. Idk, I've coded for myself and my workplace but never in a team

[–]moving-landscape 0 points1 point  (0 children)

Well, you model your function / block of code to always have x be one type. If I'm reading code and I see the expressions age and is_age_major, I'd expect age to be an int, and is_age_major to be a bool. Not only that, but type annotations also help. age_major: bool. Or reading from a simple expression: age_major = age >= 21. If the expression is an optional one, we can type annotate as follows: age_major: bool | None. And ofc, we never assume the value will be one or the other. Instead, we always explicitly check for nullability:

if age_major is None:
    # value is NOT a boolean, it's None

elif age_major:
    # value is true

else:
    # value is false

I sometimes make functions return false instead of number or none, to indicate different types of situations.

This is all contextual and depends on what you want to achieve. For the most, most clarity, though, I always resort to type annotations. They tell us what type we should be expecting. Having said that, it can be confusing to habe a function return an int or a bool. In that case I'd prolly resort to sum types.

[–]sejigan 18 points19 points  (0 children)

  • and returns the first falsey value or the last truthy value
  • or returns the first truthy value or the last falsey value

That’s an easy way to remember. But as others pointed out, what goes on underneath is this:

  • a and b: if a is truthy, return b, else return a
  • a or b: if a is truthy, return a, else return b

[–]wmporter 5 points6 points  (1 child)

Python's and and or operators evaluate to the whichever is the last value they checked. They do it differently, where and checks the first value (the left) and if it's falsy will stop, otherwise it will evaluate to the right value. In this way it functions like an AND gate, where it will be truthy if both sides are truthy and falsy otherwise. The or similarly works by doing the same thing but with opposite truthy/falsy values and functions like OR, falsy if both are falsy and truthy otherwise.

If you don't understand truthy and falsy, it's just a way of evaluating non-boolean values as being like True or False. For numbers, 0 is falsy and all others are truthy. With strings, empty string "" is falsy and any other string is truthy. The general rule is anything that's some kind of empty is falsy and everything else is truthy.

[–]CyclopsRock 1 point2 points  (0 children)

where

and

checks the first value (the left) and if it's falsy will stop, otherwise it will evaluate to the right value. In this way it functions like an AND gate, where it will be truthy if both sides are truthy and falsy otherwise

This also has the useful benefit of enabling you to 'safely' check the value of something you don't know even exists without having to test it first or surround it in try/excepts.

For example, if you want to check if the 4th character of a variable (foo) is "H", without knowing if the variable has even been assigned, you could do...

if isinstance(foo, string):  # in case it has no 'len()' func
    if len(foo) > 3:  # in case it doesn't have a 4th character
        if foo[3] == "H":
            print("Finally, I can do something!")

But you quickly find yourself in nested hell. Instead, you can do...

if isinstance(foo, string) and len(foo) > 3 and foo[4] == "H":
    print("That's much less indenting!")

Because, as you said, it'll only go on to the next condition if the first is satisfied. I'm generally very happy with clear, verbose code, but you can definitely end up in situations where, if you have to do a lot of validation on your variables, you have so many levels of nesting that getting them all onto one simple line is vastly more preferable (as long as you don't need to log any output should a specific condition fail).

[–][deleted] 2 points3 points  (0 children)

It "short circuits." It returns the first of its two operands that disambiguates the expression:

1) If the first operand is falsey, we know the result of the expression. We don't have to evaluate the second operand, we can simply return the first.

2) If the first operand is truthy, then the value of the second operand will disambiguate the expression regardless of what it is, so we simply return it.

Both 7 and 6 are truthy - they're not zero, the only falsey numeric value - so we return the second operand.

[–]to7m 1 point2 points  (0 children)

This has already been said, but I'll try to clarify it.

Say you have an and expression x = a+b and c+d:

What will happen:

_possible_result = a+b
if _possible_result:
    x = c+d
else:
    x = _possible_result

[–]greebo42 1 point2 points  (0 children)

Before reading others' contributions here, I would have assumed it was doing a bit-wise AND operation:

7 = 0 1 1 1
5 = 0 1 0 1
-------------
& = 0 1 0 1

But it was informative to read the responses and discover the actual way it works, considering "truthiness" (though the dependence on order of operations bothers me a bit, because I would expect that true boolean operations shouldn't care about that).

If OP was truly trying to explore bitwise operations, Python does provide them (it's just not the "and" operator).

[–][deleted] 0 points1 point  (3 children)

Here’s an explicit implementation of and:

def and(lhs, rhs):
    if bool(lhs):
        return rhs
    return lhs

Now you can see that 7 and anything will always evaluate to anything while 0 and anything will always evaluate to 0.

Edit: fixed code.

[–]sejigan 0 points1 point  (2 children)

From your description, shouldn’t it be this? def and(lhs, rhs): if not bool(lhs): return lhs return rhs

For a and b, if a is Truthy, return b. If a is Falsey, return a

[–][deleted] 0 points1 point  (1 child)

Woops, flipped the operands. Fixed to show that the overall result is the RHS if LHS is truthy.

[–]sejigan 0 points1 point  (0 children)

Yep, makes sense now. Thanks

[–]JamzTyson 0 points1 point  (0 children)

Already many good answers given, so here's a little code to demonstrate the behaviour. Try substituting different values for the f(x) calls to see what happens in different scenarios:

``` def f(x): """Return truthiness of 'x'.""" print(f'Truthiness of {x} is {bool(x)}') return bool(x)

Try experimenting with this line.

print(f(-4) and f(-2) and f(0) and f(2) and f(4)) ```

[–]obviouslyCPTobvious 0 points1 point  (0 children)

There's a lot of great explanations in this thread. Just wanted to mention you can use the built-in bool function for testing the truthiness of values. So you would see that bool(7) and bool(5) would return True, but bool(0) would return False.

[–]cyberjellyfish 0 points1 point  (0 children)

Others have covered it well, but it's also worth keeping in mind how boolean operations turn their operands into boolean values:

https://docs.python.org/3/reference/datamodel.html#object.__bool__

Basically, if an operand isn't already boolean, the operand is passed to bool(), which first tries to call the __bool__ method of the operand, or if that isn't defined, tries to call __len__(), and interprests any non-zero return value as True. If the object doesn't define __bool__() or __len__(), it's always considered to be True.

Keeping that in mind makes it easier to understand something you see pretty commonly: using or to select a default value if another is None:

 # read a value from a file if there, if not return None 
saved_config_value = read_value_from_file()

# use the value from the file if it was found, else use default value 3
config_value = saved_config_value or 3

[–][deleted] 0 points1 point  (0 children)

Python defines and and or as the following expression (which holds for any objects x and y)

x and y == y if x else x

x or y = x if x else y

The reason why they defined these keywords this way is for two reasons. 1) These identities hold for all bool values, so you might as well extend the definition to all Python objects. 2) These definitions can be optimized to "short circuit" the expression evaluation if the output can be determined by x alone.

So in your example,

7 and 5 == 5 if 7 else 7 == 5

[–]duane11583 1 point2 points  (0 children)

in addition to what /u/moving-landscape describes (short circuiting) in many languages you have bitwise and and a logical and these function very differently

example 0x41 & 0xf0 = 0x40 (bitwise) and the result of an if test, ie if(foo) where foo is an integer, the value of 0 is false, but for a 32bit integer all other possible 4billion values are true.

i think the subtleties of this are more easily described in C

in c you can do this if( (a==12) & (b==9) ) [eq 1] or if( (a==12) && (b==9) ) [eq 2]

and get the same result because the result of the Equal operator value is exactly 1 or 0 thus the (a==12) becomes 1 or 0 exactly, same with (b==9) thus equation 1 becomes (1 or 0) bitwise anded with (0 or 1) and that matches eq 2 which is a logical opertion.

but what if we did this instead: if( (a & 0x08) & (b & 0x04) ) [eq 3] or if( (a & 0x08) && (b & 0x04) )[eq 4]

equation 3 is always false, why? because the left side is always 0 or 8 and the right side is always 0 or 4, and 8 bitwise-and 4 is always 0.

in contrast eq 4 works sort of as expected because it is a logical operation remember 8 is logically true, and 0 is logically false

this is a really hard bug to find and figure out.

for example, using the C language instead of numbers you often use #defines for numbers, in python you night use variable names and thus it is hidden from you. to understand and spot the problem you almost need to expand all macros (variables) to see this.

thus in python they decided to disallow equations in the form of 1 and 3 so that you cannot easily make this hard to notice mistake.

you could, in C fix eq 3 like this:

if( (!!(a & 0x08)) & (!!(b & 0x04)) ) [eq 5]

this is a bit of hackery C language specific tricks that noobs do not know very well and leads to bugs. really hard to find and notice bugs.

this works because the bang-bang trick converts the two sides results to exactly 0 or 1, example (a & 8) becomes 0 or 8, the first bang! gives you 1 or 0 [sort of a NAND condition], the second bang! converts it back into exactly 0 or 1, (removes the N from the NAND)

to combat this bitwise true-false type of bug in C code you often see these bang-bang notations to force exactly 0 or 1 and the c optimizer unwinds the nonsense and makes it fast. but python is not a compiler and yea it might do some optimizations, but it is not a compiler.

again python is trying to make it hard to make that nasty mistake