Why this matters
A function often receives a value whose exact type it does not know. A function that prints a length must first check that it got text, and not a number or None. Python has no instanceof keyword. The function isinstance does that job, and it returns True or False.
Python also has a match statement. A pattern in a match can test the type of a value and give the value a name in one step, which saves a second line. A separate tool, the walrus operator :=, gives a name to a value inside a condition. Python has no statement that declares a variable inside an if, so the walrus is the closest tool.
This lesson covers those three tools and one rule that is different from Java. Python has no block scope for an if. A scope is the part of the code where a name can be used. A name set inside an if body stays in place after the if. What decides whether the name has a value is whether the line that set it ran. The if statement itself is covered in the earlier lessons of this module, and this lesson does not repeat them.
The examples were run with Python 3.14.4. The error messages in this lesson show the file name only, without the folder path in front of it.
The older way: isinstance
The function isinstance(value, str) returns True when value is text. The second argument is a type, such as str for text, int for a whole number, or list for a list. The next function chooses a message from the type of its input, and it uses isinstance for each choice:
a string of length 5
an integer, doubled: 42
something else: NoneEach branch works with a value of the right type. The function len works on value only in the first branch, because the test already passed.
The test is not as strict as it looks. The type bool is a kind of int, so isinstance treats True and False as whole numbers:
True
TrueThis matters when you check for a number. The next two sections show how to handle it.
The idea: a match that also names the value
A match statement compares a value with a list of patterns, one after another. A pattern can be a type, followed by a name written after as. The pattern in case str() as text has three parts. The first part is the type, str. The brackets are needed, because they make it a class pattern that checks the type. The second part is as. The third part is the name, text, which holds the value when the pattern matches.
The last case, case _:, matches anything. It works like the else branch of an if.
a string of length 5
an integer, doubled: 42
something else: 2.5
something else: NoneThe name text holds the same string as value. The name number holds the same integer as value. Both names are ready to use on the next line, and no separate conversion is needed.
The bool trap from the previous section also applies here. A case int() pattern matches True. To handle booleans first, put a case bool() line above the int line:
a boolean: True
an integer, doubled: 42The patterns are tried from top to bottom, and the first match wins. A bool pattern placed below the int pattern would never run for True.
Where a name can be used in Python
In Java, a name made in an if test is visible only in certain places. Python works differently. A name set inside an if body belongs to the whole function, or to the whole file when the code is outside a function. It stays after the if, and it stays in the else branch too. What matters is whether the line that sets the name ran.
The next function sets text only when the value is a string. It then prints text after the if:
def show(value):
if isinstance(value, str):
text = value
print("after if:", text)
show("hi")
show(42)after if: hi
Traceback (most recent call last):
File "scope_function.py", line 8, in <module>
show(42)
~~~~^^^^
File "scope_function.py", line 4, in show
print("after if:", text)
^^^^
UnboundLocalError: cannot access local variable 'text' where it is not associated with a valueFor "hi", the assignment ran, so text exists and the first line prints. For 42, the assignment did not run. Python still knows that text belongs to the function, so the error is UnboundLocalError. The message says that the name exists but has no value yet.
At the top level of a file, the same mistake gives a different error:
value = 42
if isinstance(value, str):
text = value
print(text)Traceback (most recent call last):
File "scope_file.py", line 4, in <module>
print(text)
^^^^
NameError: name 'text' is not defined. Did you mean: 'next'?The name text was never created, so Python reports a NameError. Both errors come from the same rule. A name exists only after the line that creates it has run.
Python has no braces that group a block of statements, and an if body does not open a new scope. If you want a name to exist only in one part of the code, put that part in a function. The name then disappears when the function ends.
The walrus operator :=
The walrus operator := assigns a value to a name and also gives that value back, so it can be used inside a condition. The name is written on the left, and the value on the right. The parentheses in the next example are needed. Without them, the comparison > 3 would be part of the assigned value, and n would hold True or False instead of the length.
long word, 7 letters
n is still 7The first line gives the name n the length of word and then compares it with 3. The if body uses n, and the line after the if can use it too, because n belongs to the file.
The walrus can also be used inside a while condition, but this lesson does not cover loops. The module on loops covers them.
Early return with a guard
A guard clause is a check at the top of a function that returns early when the input is wrong. The rest of the function then needs no extra nesting. This function returns the length of a string, or -1 when the value is not a string:
4
-1The line return len(value) runs only when the if body did not run. That means value is a string at that point.
Matching a class with a guard
A class pattern checks the type of an object and then checks the values stored in it. The next example uses two small classes made with @dataclass. The line @dataclass is a decorator, a line that starts with @ and changes the class below it. It tells Python to write the __init__ method for the class, so each class stores its fields from the arguments.
A case line can end with if, which is called a guard. The guard runs only after the pattern has matched:
large circle, radius 12.5
small circle, radius 3
square, side 4
not a shapeThe first case matches a circle with a radius above 10. The second matches any other circle. Python does not convert the numbers it stores. The circle with radius 3 keeps the whole number and prints 3, while a plain Java double field would print 3.0. The type hints float on the fields do not change the stored value.
Writing equals with a type check
Python lets a class decide what == means. The method __eq__ runs when two objects are compared with ==. Its parameter other can be any value, so the method must check the type before it reads fields from other.
If the type is wrong, the method returns NotImplemented. That value tells Python "I do not know how to compare with this", so Python tries the other side of the comparison. If the other side also gives up, Python falls back to comparing object identity, which is False for two different objects:
True
False
False
FalseThe first comparison is True because both points hold the same values. The string and None cases never reach the field comparison, because the type check fails first. A class that defines __eq__ also needs a __hash__ method before it can be used in a set or as a key in a dict. The common mistakes section at the end shows the error that appears without one.
A name set inside an or test
The next function returns the first word of a string. It returns "none" when the value is not a string, or when the string is blank. The name text is set inside the or test, and it is used after the if:
hello
none
nonePython evaluates the left side of or first. For 42, the left side is True, so Python never runs the walrus and text is never set. The if body returns, so the line that uses text never runs in that case either. For " ", the walrus sets text to an empty string, which is false, so the body returns "none". For "hello world", text holds the stripped text, and the function returns the first word.
This works because the if body always returns. If the body only printed a message, the line return text.split(" ")[0] could run with text never set, and Python would raise an error.
Common mistakes
Using the name in the else branch
The else branch runs when the test is false, so the assignment inside the if body did not run. The next program has this mistake:
value = 5
if isinstance(value, str):
text = value
print("yes")
else:
print(text)Traceback (most recent call last):
File "else_branch.py", line 6, in <module>
print(text)
^^^^
NameError: name 'text' is not defined. Did you mean: 'next'?The fix is to use value in the else branch, which holds the value itself, or to write a second test for the other case.
Using a bare name in a case line
A case line with a bare name, such as case str:, does not check the type. It is a capture pattern, which matches any value and gives it the name str. Python rejects this in the next program, because the case _: line below it can never run:
def describe(value):
match value:
case str:
return "a string"
case _:
return "something else"
print(describe(42)) File "bare_case.py", line 3
case str:
^^^
SyntaxError: name capture 'str' makes remaining patterns unreachableThe fix is to add the brackets: case str():. The brackets make the pattern a type check, and the name str stays the built-in type.
Writing a generic type in isinstance
The type list[str] describes a list that holds text, and Python can write it. The function isinstance cannot check the items inside a list, so it refuses the type with brackets:
value = ["a", "b"]
print(isinstance(value, list[str]))Traceback (most recent call last):
File "generic_type.py", line 2, in <module>
print(isinstance(value, list[str]))
~~~~~~~~~~^^^^^^^^^^^^^^^^^^
TypeError: isinstance() argument 2 cannot be a parameterized genericThe fix is to check the outer type with isinstance(value, list), and then check each item in a loop or with a comprehension.
Passing a name as text
The second argument of isinstance must be a type, not a string that names one:
value = ["a", "b"]
print(isinstance(value, "list"))Traceback (most recent call last):
File "string_type.py", line 2, in <module>
print(isinstance(value, "list"))
~~~~~~~~~~^^^^^^^^^^^^^^^
TypeError: isinstance() arg 2 must be a type, a tuple of types, or a unionThe fix is to remove the quotes: isinstance(value, list).
Using := as a plain statement
The walrus operator is meant for a condition or an expression. Written alone, it is a syntax error:
n := 5
print(n) File "walrus_statement.py", line 1
n := 5
^^
SyntaxError: invalid syntaxWrite n = 5 for a plain assignment. Parentheses make the walrus an expression that can stand alone, as in (n := 5), but a plain = is clearer.
Making a class unhashable by defining equals
A class that defines __eq__ and no __hash__ cannot be used in a set or as a dict key. Python sets its __hash__ to None in that case. The next program calls hash on an object of such a class:
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
def __eq__(self, other):
return self.x == other.x and self.y == other.y
print(hash(Point(1, 2)))Traceback (most recent call last):
File "unhashable.py", line 10, in <module>
print(hash(Point(1, 2)))
~~~~^^^^^^^^^^^^^
TypeError: unhashable type: 'Point'The fix is to define __hash__ in the class. In the Point class from the equals section, that method returns hash((self.x, self.y)). The tuple (self.x, self.y) holds the same values that __eq__ compares, so two equal points give the same hash. With that method added, len({Point(1, 2), Point(1, 2)}) gives 1.
Practice
Each exercise has its answer below it. Try to work it out before you read the answer, and run the code if you can.
Exercise 1
Write a function sum_integers(items) that adds up all the whole numbers in a list and ignores the other values. A bool is not counted as a number here. Call it with [1, "two", 3, 4.0, 5, True].
Show the answer
Use isinstance for the type, and add a second test that rules out bool:
9The values 1, 3 and 5 are added. The string, the float 4.0 and the True value are skipped. The for loop runs its body once for each item in the list.
Exercise 2
Write a function count_digits(value) that returns the number of digit characters in a value, and returns 0 when the value is not a string. Call it with "a1b22c" and with 7.
Show the answer
The name value is checked with isinstance, and the loop uses it only after the check:
3
0The string "a1b22c" has the digits 1, 2 and 2. The method isdigit returns True for a character that is a digit.
Exercise 3
Write a test that prints non-empty: followed by the text when value is a non-empty string, and prints empty or not a string otherwise. Start with value = "".
Show the answer
Use and, so the second part runs only after the type check has passed:
empty or not a stringThe version with match uses a guard. The case matches a string, and the guard if text checks that it is not empty:
empty or not a stringThe empty string is false, so both versions take the else branch.
Exercise 4
Predict the output of this program, then check it.
Show the answer
The output is 6 and then done. The call run(5) passes the type check, so the function prints 6. The call run("x") fails the check, so the return leaves the function without printing anything. The line print("done") runs after both calls.
Exercise 5
Write a function describe(value) that returns list of N items when the value is a list, where N is its length, and returns not a list otherwise. Call it with [1, 2, 3] and with "abc".
Show the answer
A class pattern with a name does the type check and gives the list a name in one step:
list of 3 items
not a listPython needs no extra detail for the items here, because a list object does not carry a type for its items at run time. A case list() pattern checks the outer type only.
Summary
- Use
isinstance(value, type)to test a type, and remember thatboolcounts as anint. - A
matchpattern such ascase str() as texttests the type and names the value in one step, and acase _:line handles everything else. - A name set inside an
ifbody stays visible after theifand in theelsebranch. Its value exists only if the line that set it ran. - The walrus operator
:=gives a name to a value inside a condition, as inif (n := len(word)) > 3:. - Python has no declaration that limits a name to an
if. Put the code in a function if you need that. - A
caseline needs brackets for a type check, as incase str():, andisinstancetakes a type, not text or a parameterized type such aslist[str].