 While Python does have a Boolean type, it has the values true and false. For the sake of condition expressions, like say the condition of an if or a while, those expressions don't necessarily have to evaluate into Boolean value. They can actually evaluate into any kind of value. And this is the case because for the purpose of a condition, Python considers all objects to have a truth value. And obviously the Boolean value false has the truth value of false and the Boolean value of true has a truth value of true. But what about other objects? Well first off, the special value none is a special case that's always considered false. But all other objects, such as say the instances of a class you create, those are by default considered true. If however an object has a special method bool, double underscore bool, double underscore, or a special method len, short for length, then one of those two methods is used to determine the truth value of that object. The bool method of an object should return either true or false, and that returned value represents the truth value of the object. If however an object has no bool method and it only has a len method, then the len method is invoked and if it returns zero, that's signifying that the object has a truth value of false. Any other returned value signifies a truth value of true. So for example these standard number types of Python have a bool method but no len method, and so their truth value is determined by invoking this bool method, and the way that bool method is defined for the number types, it will only return false if the number is equal to zero. Effectively then, all number objects in Python have a truth value of true, except those which are equal to zero. As for the len method, well the standard collection types, the list type, the dictionary type, and the related types, they all define a len method but no bool method. So when Python wants to determine the truth value of say a list, it invokes its len method and if len returns zero, then the list is considered false. And of course the len method is short for length, so it's returning the number of items in that collection. So effectively list, dictionary, and string objects in Python are all considered true except those which are empty. So if you have an empty list, an empty dictionary, or an empty string, their truth value is false. So to sum it up again, all objects in Python have a truth value of true, except for the booling value false, the special value none, numbers equal to zero, empty strings, and empty collections, and for the sake of any classes you yourself define, the truth value is determined by the bool or len method, and if you give your object both the bool and len method, the bool method is used to determine the truth value instead of len. Whereas in JavaScript, and Java, and C, and in fact most popular languages, the and and or operations are represented by double ampersands and double pipe symbols. In Python they're represented by the reserved words and and or. And likewise instead of an exclamation mark for the not operator, not is just written as the reserved word not. Now when it comes to the actual operation of these operators, the not operator acts like you expect. It returns the inverse truth value of whatever the operand is. So here if x is true, then not x will return false. And if x is false, then, or has a truth value of false, then not x will return the booling value true. But as for the and and or operators, they actually have a short circuiting behavior, just like they do in JavaScript. So surprisingly in some cases, only the left operand is evaluated. The right operand might go un-evaluated. So if the right operand is a function call, well that function call may not get invoked. Also surprising is that the and and or operators don't necessarily return a boolean value. They don't necessarily return true or false. Instead they return one of the two operand values. So in the expression x and y, Python will first evaluate just the left operand here, the expression x. And if the value of that expression has a truth value of false, then that object is what is returned by the expression x and y, and y doesn't get evaluated at all. However, if the value returned by the left operand expression here, x, has a truth value of true, then the right operand expression here, y, will get evaluated and the value of that expression, the right hand operand, is what's returned by the and operation. So to put it succinctly, if x is false then we return x, but if x is true then we return y instead. Now if you think about it for a moment, this makes sense. Because if in an and operation you test just one of the operands and discover that it has a truth value of false, well then you don't need to evaluate the other operand. You already know that the and operation is supposed to return a false value. And moreover, instead of necessarily returning the boolean value false, we can return the value of that first expression itself, because we already determined that, hey, it has a truth value of false. So whatever object that is, is a suitable substitute for the boolean value false. Now in the case where the left hand operand evaluates true, well at that point we then know that the value of the whole operation, the and operation is equivalent to the value of just the right hand operand by itself. If x is true and then it turns out y is also true, then we know that the and operation should return true, but if x is true and then y is false, then the and operation should return false. So the correct truth value of the whole and operation is synonymous with the value of just the right hand operand by itself. So in the expression x and y, we evaluate x, and if it turns out to be true, then we evaluate y and return that object as the result of the and operation. As for an or operation, everything works out the same except we just invert the truth test. So when we evaluate that left hand operand and discover that it's true, then we return the left hand operand itself, otherwise if it's false, then we evaluate the right hand operand and return that as the value of the expression. Because if you think about it for a moment, if we're testing whether x or y is true, once we've tested x and discovered that it is true, we don't need to know what y is. It can be true or false and the value of this or operation is still going to be true no matter what. However, if x turns out to be false, well, it's still possible for y to be true or false and that still matters because if it's true, then the or operation should return true, otherwise it should return false. For a quick example here, we have an or operation with a left hand operand of just 3 and a right hand operation that invokes foo. What happens here is at first the left operand expression is evaluated and its truth value determined and 3 has a truth value of true because all nonzero numbers have a truth value of true in Python. And so at this point, having only evaluated just one of the operands, we already know that the or should return true, so we don't need to evaluate the other one. We don't have to actually call the function foo here. So this or operation will return the value 3 without actually evaluating here the call to foo. It's always important to distinguish in any language between an equality test and an identity test. An equality test is asking whether these two objects have the same content effectively, so two number objects can have the same value. They can have an integer value 3 and a floating point value 3.0 and consider them to be equivalent even though they aren't the very same value. An identity test in contrast is not asking whether two objects have the same content or whether they represent the same value. It's asking whether these two objects are actually one and the same object. So with the important difference in mind to perform an equality test in Python, the operator is a double equal sign like what you find in most languages and in the logical inverse, the not equals operator is written with an exclamation mark equal sign. And like most operators in Python, these two operators are really method calls in disguise. The equality operator invokes the EQ method whereas the inequality operator, the not equals operator invokes a method called NE as in not equals. As for identity tests, the operator is written as the reserved word is and then the logical inverse of the is operator is written as is not. And be clear that though is not is written as two separate reserved words, it's really one single operator. You shouldn't think of it certainly as the is operator followed by a not operator. That's not what's going on here. It's just one operator that happens to have a space in the middle. Actually, if here in this example we were to put parentheses around the not y portion, then that would be a use of the not operator producing some Boolean value, either true or false, and then we'd have another is operation, x is either true or false. And in that case, yes, it would be two separate operators, but here without any parentheses, separating is and not, they're together treated as one operator. Now if you're wondering why the identity operators here is and is not, do not have corresponding methods like the equality operator does or the not equality operator does. Well, the answer to this is that object identity is not a type dependent concept. What it means for any two objects to be identical doesn't change depending upon the types of the objects. It's not like some class you define is going to have its own special notion of what identity means. That's not the case. Whereas with equality, that may very well be the case. For example, in the case of numbers, there are cases where two numbers can be equivalent without them being exactly the same. For example, the integer value three and the floating point value 3.0, those two values we consider equivalent, even though they're not precisely the same. And so what you may find when you create your own data types, your own classes, you may want to have some special definition of what equality really means for that type. So for your own class, you may want to define what the equality and inequality operators mean. Whereas you wouldn't want to do that with the identity operator. There's no reason you would ever want identity to mean something other than just these two objects are precisely the very same objects in memory. Now somewhat confusingly, if you define a class and you don't give it its own EQ and NE methods, well then that class will simply inherit the methods from object. And the way these two methods are defined in the object class, they return true when the objects are identical. So effectively by default, the equality operator used on class instances does the same thing as an identity test. Now, when it comes to the relational equality and identity operators, Python has a special rule when you have them one after the other, when you have them in a chain. Looking at this example JavaScript code, when you see x less than y less than z, that's almost certainly a mistake. Because when you write x less than y less than z, what you expect that to do is to return true if x is less than y and y is less than z, but that's not how JavaScript interprets it. In JavaScript this would be executed as first x less than y returning a Boolean value and then the other less than operation, the whatever less than z doesn't make any sense anymore because you can't use a Boolean value in a less than comparison. So the proper way to express this in JavaScript is to write x less than y and y less than z. It's really two separate comparisons and we logically end them together. In Python, however, you can actually get away with expressing the same logic either way. You can do it as you would expect to write it in JavaScript or in fact most other programming languages. You could write x less than y and y less than z, or you could just write x less than y less than z because Python has a special rule for chained relational operators. Relational operators appearing one after the other, not separated by parentheses. And what this special chaining rule does is it takes an expression of this kind and splits it up into separate relational operations connected by the AND operator. So Python takes x less than y less than z and magically converts it into x less than y and y less than z. And this special chaining behavior works not just with relational operators like less than or greater than or less than or equal to. It works also with the equality operator, the double equal sign or the not equality operator. And it also applies to the identity operator, so is or is not. And be clear that this rule applies when you have a mix of these different kinds of operators. So you can write a not equals b greater than equal to c is d and Python will magically insert as many AND operators as is necessary. Now the only thing to watch out for here is that occasionally it's conceivable at least you can have a scenario where you don't want this magic to be applied. And the way you suppress it from being applied, the special chaining rule, is just use parentheses to make your intention explicit and then Python won't insert any AND operators. To be honest, I actually wish Python didn't have this special chaining syntax because it's really unexpected coming from most other programming languages. I tend to avoid it and just make all my AND operations explicit and I believe most Python programmers do the same. Python has another syntactical allowance which is usually called chained assignment but don't let the name confuse you because it doesn't really have anything to do with the chained relational equality and identity operators. This is just really the allowance in Python that lets you write x equals y equals z equals four. Remember that unlike in most languages, Python doesn't allow assignment as an operation. When you write an equal sign in Python for assignment, that's a special kind of statement, not an operation. So in Python you can't write something like what you see here in the second line where we have this y equals two used as an expression, as an operation that returns a value because in Python that's just not the case and assignment is a particular kind of statement. However, it is a nice convenience to be able to assign a single value to multiple variables in one statement and so for that, Python has the special chained assignment syntax. Just be clear that in Python, assignment is a statement, not a kind of operation. Unlike in JavaScript where all numbers are stored internally as floating point values as the same data type, in Python, integer values and floating point values are separated by two separate data types. The integer type is called int and the floating point type is called float. So if you see a number literal reading 3.6, that's a float value 3.6, and if you see a number literal like six with no decimal point, that's an int value. Now the class objects representing these two data types are found in the built-in namespace, predictably assigned to the references int and float. If you have a float value and you want the corresponding integer value, you can pass that float to the int constructor and that will return an int value which is the rounding of that float. So int 3.6 will return four and conversely if you want a float value from an int value, then you pass the int to the float constructor and that will return the float equivalent. So float 6 returns a floating point value of 6.0. Now an important thing to keep in mind is how these two data types are represented. Float values are represented in the IEEE 64-bit floating point format, just like in JavaScript. This means that when it comes to floats, you're dealing with limited precision and magnitude. Float values can only get so accurate and so large. For most purposes, these limitations aren't really relevant, you're not going to bump into them, but depending upon what you're doing, if you're doing highly precise math calculations, it's something you may need to keep in mind. Python's int type, on the other hand, has no size constraint, meaning Python will use however many bits are necessary to represent an integer, even if that means creating integers that take up megabytes of memory, which would be very extreme and not something to occur commonly, but Python is capable. So for all intents and purposes in Python, whatever integer value you want, and the only limitation is you can't deal with integers so large that they exceed the entire storage capacity of your system, which frankly would be a very absurd thing to happen. Now, in addition to ints and floats, there's actually a third number type for complex numbers called complex, and the way to express a complex number as a number literal is to write the imaginary portion suffixed with a j. Why the letter j and not i? I believe that's simply because it was decided that the letter i in a lot of programming fonts doesn't stand out well enough. It looks too much like an L or a 1, so instead they go with j. In any case, if you then want to write a complex number with a real portion, then you just add a regular integer to the imaginary number, and that will return a complex number. Alternatively, if you write complex, which is found in the built-in namespace, you just write complex and pass the real portion as the first argument and the imaginary portion as the second argument, then that returns also a complex number. Python also has a syntax for expressing int values in hex, octal, and binary, and these are written simply by professing the number with 0 followed by the letter x for hex, o for octal, or b for binary, and those letters can be uppercase or lowercase, though I think lowercase is generally more readable. And then also if you want to express float values in engineering notation, a.k.a. scientific notation, then you just write the mantisa portion, the significant, followed by e, and then the exponent. While the normal division operator is expressed as just a single slash, there's also a double slash operator which does what Python calls floor division, and floor division simply rounds the answer down to the next integer. So 8 double slash 3 returns the int value 2, not 2.66666 as you would normally expect with regular division. And then same deal with 8.0 double slash 3 returns 2 except this is a floating point value because the operands here included a float. And generally anytime you mix a float and int operand together in an operation it'll return a float. And then finally the last example, negative 8 double slash 3, notice that it rounds down from negative 2.666 to negative 3. So it's not rounding towards 0, it's always rounding down to more negative. Now one of the biggest gotchas in Python is actually something which Python 3 fixed. But the gotcha in Python 2 and earlier was that the regular division operator when both operands were ints it would return an int value. Even if the correct answer, the mathematical answer you would expect to be a float value. So for example, 8 divided by 3 in Python 2 would return an int value 2. And the trick you had to use to get the answer which you normally wanted was to make sure one of the operands was a float rather than an int. So for example, if you write 8 divided by 3.0, well 3.0 that's a float so the answer we get back is going to be a float and it's going to be correct, it's going to be the float value 2.6666.