Python First

Expressions

2026-08-01

Abstract

Almost every statement contains an expression, which we indicate in syntax as expr, and sometimes refer to as expression in paragraph text. Expressions can be simple (single value), or complex (contain operators). The operands of operators are expressions, which means that an expression, can contain sub-expressions.

Expression Features

An expression is a programming construct that ‘has a value’. To be more exact, an expression results in one, and only one, value. As we have seen, any value has a type, so it is reasonable to say: an expression has a value and a type:

     expr   ⇒   value + type

If an expression contain operators, those operators will require operands. Some operators require one operand (unary operators), and some require two (binary operators). Not all operators work with all types of values.

When an operator can work with multiple types of operands, we say it is overloaded. For example, the addition operator (+), is overloaded for str types, which will perform concatenation when its operands are str types, instead of addition.

The order in which expressions are evaluated, depends on the precedence of the operators. When two operators have the same level of precedence, Python chooses one based on their associativity with their operands. Most operators associate from left-to-right (L→R), while the exponentiation operator associate from right-to-left (R→L).

Operator Precedence

Operators are listed below from highest to lowest precedence. Note that some operators are not symbols, but keywords. The expr parts below must be an appropriate type for the relevant operators. It can be a simple object.

operators description
(expr), [expr], {key:val}, {expr} Force precedence; list, dictionary, set literals
expr[index], expr[index:index], expr(arg,), expr.attr Subcript, slice, function call, attribute reference
await expr Await asynchronous expression.
num**exp Exponentiation: raise num to exp.
+expr, -expr, ~expr Unary plus, minus, bitwise NOT
expr*expr, expr**@**expr, expr/expr, expr//expr, expr%expr Multiplication, matrix multiplication, division, integer division, remainder (modulus)
expr+expr, expr-expr Addition, subtraction (difference)
int<<int, int>>int Bitwise left shift, right shift
int&int Bitwise AND
int^int Bitwise XOR
int|int Bitwise OR
in, not in, is, is not, <, <=, >, >=, !=, == Membership (in), identity (is), comparisons, all returning bool
not expr Logical (boolean) NOT
and Logical (boolean) AND
or Logical (boolean) OR
if-else Conditional expression
lambda Lambda expression
:= Assignment expression

Note that normal assignment is a statement, where the right hand expression is evaluated first. This must not be confused with :=, a newer assignment expression.

As a matter of good convention, we tend to put spaces around binary operators, except the attribute reference (.) and exponentiation (**) operators. No spaces after unary operators, or before the subscript operator.

Operator Implementation

Most operators are really just ‘disguised’ function calls, with a convenient and compact syntax. Each operator correlates to a special method, and a custom class can provide an implementation for each, to achieve operator overloading. Here is a table:

Operators Special Methods
** __pow__
+ __add__
- __sub__
* __mul__
/ __truediv__
// __floordiv__
% __mod__
@ __matmul__
<< __lshift__
>> __rshift__
& __and__
^ __xor__
\| __or__
== __eq__
!= __ne__
< __lt__
<= __le__
> __gt__
>= __ge__
+ (unary) __pos__
- (unary) __neg__
~ (unary) __invert__
[] __getitem__, __setitem__

Please note that the logical operators (and, or, not) and membership testing operators (in, not in) are not associated with special methods, as they rely on the boolean values or other special methods of the objects they operate on.

The reason for this design choice, is that it allows classes to overload these operators. Although not implemented as a function, we can even overload the __call__ special method to make objects callable.

Operator Module

The standard operator module provide a convenient way to ‘call’ intrinsic (built-in) operators as functions. This is mostly useful if you want to a pass an operator to a function that requires a callable argument.

If this module has been imported, the function call ‘operator.add(a, b)’ is equivalent the operator notation: ‘a + b’.

pyOperator module functions

import operator, functools
result1 = operator.add(12, 34)
result2 = 12 + 34
print(result1, result2)             #→ 46 46
total = functools.reduce(
   operator.mul,                    #← pass `*`
   [11, 22, 33, 44])
print(total)                        #→ 351384

Arithmetic

Operators that should be familiar to you, are the arithmetic operators: multiplication (*), division (/), addition (+) and subtraction (-). They work on both int and float type operands, i.e., numeric types.

When mixing int and float operands with arithmetic operators, they will first convert the int operand to float, before evaluating the operator. The result will thus be float.

pyImplicit arithmetic conversion

>> print( type(12.34 * 2) )  #→ float

What is unusual, compared to some other languages, is that the division operator always returns a floating point result; even if both the operands are of type int.

pyFloating point division

>> print( 2 / 3 )            #→ 0.6666666666666666 
>> print( type (2 / 3) )     #→ float 

For situations where this is undesirable behaviour, you must use the integer division operator: //. It always returns an integer value, though not necessarily of type int… but even if the result is float, the significant digits will be .0.

pyTrue division vs floor division

>> print ( 12   /  5 )       #→ 2.4    ← float
>> print ( 12   // 5 )       #→ 2      ← int
>> print ( 12.0 /  5 )       #→ 2.4    ← float
>> print ( 12.0 // 5 )       #→ 2.0    ← float

Also unusual, is the modulus or remainder operator (%). It surprisingly works with float and int values. In other languages, it tends to work only with integer types in those languages. It works like division, but returns the remainder, instead of the division result.

pyModules/remainder operator

>> print( 22 // 5 )          #→ 4      ← division.
>> print( 22 %  5 )          #→ 2      ← remainder.
>> print( 1.5 / 5 )          #→ 0.3    ← division.
>> print( 1.5 % 5 )          #→ 1.5    ← remainder
>> print( 1.5 % 1.25 )       #→ 0.25   ← remainder

Python also has the exponentiation or ‘to the power’ operator: **. It can raise a numeric operand to a power. The power can be a float, and even negative (inverse).

pyExponentiation operator (to the power)

>> print( 2.5 ** 2  )        #→ 6.25   ← 2.5 × 2.5
>> print( 2.5 ** 3  )        #→ 15.625 ← 2.5 × 2.5 × 2.5
>> print( 2.5 ** -2 )        #→ 0.15   ← 1 ÷ 2.5 ÷ 2.5
>> print( 2.5 ** -3 )        #→ 0.064  ← 1 ÷ 2.5 ÷ 2.5 ÷ 2.5
>> print( 2.5 ** 0.5)        #→ 1.581… ← math.sqrt(2.5)

Spaces around the exponentiation operator is optional.

Comparisons

We can compare two expressions for equality using: == (equality operator). We can establish inequality with: != (inequality operator). These operators always return a bool (boolean) result: True or **False*.

pyEquality and inequality operators

>> print( "ABC" == "abc" )           #→ False
>> print( "ABC" != "abc" )           #→ True
>> print( "ABC" == "ABC" )           #→ True
>> print( 1.0   == 1     )           #→ True
>> print( 123   != 124   )           #→ True

When ordering becomes important, we can use the less-than (<), less-than or equal (<=), greater-than (>) and greater-than or equal (>=) operators. Like the equality operators, they only return a boolean result.

pyOrdering comparison operators

>> print( "ABC" >  "abc" )           #→ False
>> print( "ABC" <  "abc" )           #→ True
>> print( "ABC" >= "ABC" )           #→ True
>> print( 123   <   123  )           #→ False
>> print( 123   <=  123  )           #→ True

The Unicode values for upper case letters are smaller than those of the lower case letters.

Membership

Membership operators are used to test whether a value is a member of a sequence, such as a string, list, tuple, or set. The two membership operators are in and not in, and both return boolean values True or False.

A common use case, is to check if a substring is present in a larger string, without resorting to regular expressions.

pyTest for substring

text = "Python is FUN!"
print("P" in text)                   #→ True
if "FUN" in text: print("YES")       #→ YES
print("Z" in text)                   #→ False
if "Python" in text:
   print("Indeed")                   #→ Indeed

Since a string is just a special sequence, the in operator will work with other sequences. It can test sequences within sequences as well, but the types must match.

pyMembership test in sequences

primes = { 2, 3, 5, 7 }              #← set
print(3 in primes)                   #→ True
print(4 in primes)                   #→ False

fruits = [ "apple", "banana", "cherry", "date" ]
print("banana" in fruits)            #→ True
print("eggfruit" in fruits)          #→ False

pairs = ( ["apple", "red"],
          ["banana", "yellow"],
          ["cherry", "red"],
          ["date", "brown"], )

if ["cherry", "red"] in pairs:
   print("Cherries are red")         #→ Cherries are red
if ("cherry", "red") in pairs:
   print("Cherries are red")         #· Not matched.

The last examples shows that the tuple ("cherry", "red") will not match the list ["cherry", "red"].

The inverse of in is not in (you cannot use ‘in not’).

Identity

Identity operators are used to compare the memory addresses (i.e., references) of two objects to determine if they refer to the same object. The two identity operators are is and is not. They employ the built-in id function, which returns a unique identification for an object. In the CPython implementation, this will be the address of the object.

Two objects may compare as equal values, but may be different objects in memory. Conversely, if objects have the same identity, they are obviously also equal.

Python will sometimes re-use existing literal objects, though this is not guaranteed, although common with the CPython implementation:

pyEqual identities for simple literals

a = "ABC"  ;  b = "ABC"
print(a is b)                        #→ True
a = 123    ;  b = 123
print(a is b)                        #→ True

In the example, the id()s of the objects that a and b reference may be the same in both cases, because CPython may have decided to reuse the same string object for both variables. But you should not rely on this behaviour.

The inverse of the is, is ‘is not’ and not ‘not is’.

pyIdentity on non-trivial objects

a = ['A', 'B', 'C']
b = ['A', 'B', 'C']
print(a is b)                        #→ False
print(a is not b)                    #→ True
print(a == b)                        #→ True

The objects referenced by a and b are equal in value, but are not the same objects — they exist at different addresses in memory. Different names can reference the same object. This can be a problem only if you think they are different:

pyNames referencing same object

a = ["A", "B", "C"]
b = a                                #← `b` is same object.
b[1] = 'X'                           #← change second item.
print(a)                             #→ ['A', 'X', 'C']

The same effect occurs when you pass an argument to a function… the reference is copied to the parameter. If the reference refers to a mutable object, the function can change the contents of the object (but not the reference).

pyPassing copies of references

def fn (param): param[1] = 'X'
arg = ["A", "B", "C"]
print(arg)                           #→ ['A', 'B', 'C']
fn(arg)                              #← or: `fn(param=arg)`
print(arg)                           #→ ['A', 'X', 'C']

If arg and param were visible in the same scope, ‘arg is param’ would return True.

Logic

The prefix unary logical operator not; and the binary logical operators and and or; also return only boolean results. They expect operands of type bool, but will accept other types, which they will automatically convert to bool.

>> print( False or  False )          #→ False
>> print( False or  True  )          #→ True
>> print( True  or  False )          #→ True
>> print( True  or  True  )          #→ True
>> print( False and False )          #→ False
>> print( False and True  )          #→ False
>> print( True  and False )          #→ False
>> print( True  and True  )          #→ True
>> print( not True        )          #→ False
>> print( not False       )          #→ True

Python does not have an xor operator, but its logical result can be obtained using the other operators with the following formula (expression):

     ( lhs and not rhs ) or ( not lhs and rhs )

Here lhs and rhs means left hand side expression, and right hand side expression, respectively.

Boolean Algebra

In boolean algebra, and (conjunction) is written as ∧, or (disjunction) as ∨, and not (negation) as ¬. Here are a few common boolean algebra laws:

These laws may help you simplify some complicated logical operations.

Shorthand

Python has a special shorthand syntax for a common scenario: checking if a value is within a certain range. For example, imagine you want to check if some value x, is within the range [AB] inclusive, here is one formula:

     x >= A   and   x <= B

Which we can rewrite as following, with the same result and meaning:

     A <= x   and   x <= B

For a non-inclusive range: (AB), the formula would become:

     A < x   and   x < B

Python allows us to shorten the last two examples as:

     A <= x <= B
     A < x < B

pyShorthand range check and equivalent

>> x = 5 ; A = 1 ; B = 10
>> print (x >= A and x <= B)         #→ True
>> print (A <= x and x <= B)         #→ True  ← better.
>> print (A <= x <= B)               #→ True  ← shorthand.

Function Call

Arguably the most common and versatile operation, is the function call. However, calls are not performed with a function call operator as in some other languages. Instead, it is a syntactic construct that works like an operator:

     callable ( [arg…] )
     result = callable ( [arg…] )

This is very similar to the operation of operators; we just use different terminology (arguments instead of operands, for example). In fact, we can even map operators to functions.

The ‘callable ()’ syntax is really shorthand for Python mechanics calling a special __call__ method.

Callables

Any expression that is callable can be invoked with parentheses following the expression. We can test if an expression can be invoked, with the callable(expr) built-in function.

Technically, Python looks for a special __call__ method in the type of the expression, and simply invokes that, passing it the arguments, and collecting its return value.

pyBuilt-in function call detail

import builtins
print(builtins.str.__call__(123))    #→ 123
print(str(123))                      #→ 123

For calling methods in custom classes the same syntax is used, but is again a convenient shorthand for a more complex operation, where Python must pass the object on which the method is called, as the first argument.

     obj.method(args)    ≡    class.method(obj, args)

That leads to a number of ways a method can be called, though in practice one should simply used the most succinct version:

pyCalling object method equivalences

class Demo:
   def method(self, param): print(f"method({param})")
obj = Demo()
obj.method("arg")                    #→ method(arg)
Demo.method(obj, "arg")              #→ method(arg)
type(obj).method(obj, "arg")         #→ method(arg)
obj.__class__.method(obj, "arg")     #→ method(arg)

An object can be made callable, if its class implements the __call__ method.

pyCallable objects

class CallMe:
   def __call__(self, param): print(f"CallMe {param}")
obj1 = CallMe()  ;  obj2 = CallMe()
obj1("ABC")                          #→ CallMe ABC
obj2(12345)                          #→ CallMe 12345
CallMe.__call__(obj1, "ABC")         #→ CallMe ABC
CallMe.__call__(obj2, 12345)         #→ CallMe 12345

This makes Python very flexible, yet the shorthand syntax forms it provides are concise and obviously preferred. We show the alternatives only as a way to understand the Python function call mechanism.

Conditional Expression

This looks a lot like the if statement, but is an expression, which means it returns a result. Statements do not return results in Python. The if conditional expression also use the if and else keywords, and will result in one of two values: true-expr or false-expr, if the condition is True or False, respectively.

     true-expr if condition else false-expr

It is similar in operation to the [conditional operator] as found in C-like languages.

pyConditional expression

>> height = 5
>> cat = "small" if height < 10 else "large"
>> print(cat)
#→ small

It is a more compact alternative to the same logic using an if statement:

pyAlternative without conditional expression

>> height = 5
>> if height < 10:
··    cat = "small"
·· else:
··    cat = "large"
>> print(cat)                        #→ small

It is similar in operation to the C/C++ conditional operator ?:, though arguably more readable.

Copyright © 2026 Codi Matters