Python lesson 2 of 9
Python Variables and Data Types
Understand what a Python variable really is, how to name one well, and how int, float, str, bool, and None behave when you check and convert types.
Published · Every example on this page was run before it was published.
Every program you will ever write has to remember something. A game remembers your score, a checkout page remembers the price of what is in your cart, a weather app remembers this morning's temperature. Python's tool for remembering is the variable, and the kind of thing being remembered — a whole number, a decimal, a piece of text, a yes-or-no answer — is its data type. These two ideas sit underneath every other thing you will learn, so it is worth getting them exactly right before moving on.
What a Variable Really Is
Most tutorials tell you a variable is a box that holds a value. That picture is comfortable, and it will eventually mislead you. A Python variable is not a container that a value gets poured into. It is a name, and that name is attached to a value that already exists somewhere in memory. The proper word for that attachment is a binding.
A better analogy is a luggage tag at an airport. The suitcase is the value. The tag is the name. You tie the tag onto the suitcase so you can refer to it later, and nothing about the suitcase changes when you do. You could tie a second tag onto the same suitcase, or move a tag onto a different suitcase entirely. The tag never holds anything — it only points.
You create a binding with the assignment operator, a single equals sign. Python reads assignment right to left: it works out the value on the right first, then attaches the name on the left to it.
temperature = 21.5
city = "Lisbon"
print(city)
print(temperature)Lisbon
21.5Because the right side is evaluated before the name is attached, a variable is allowed to appear on both sides of the same assignment. Python calculates the answer using the old value, and only then rebinds the name to the result:
minutes = 90
minutes = minutes + 30
print(minutes)120The luggage-tag picture pays off the moment two names get involved. Assigning one variable to another does not copy anything and does not link the two names together — it simply ties a second tag onto the same value. Rebinding one tag afterwards leaves the other exactly where it was:
first_score = 88
second_score = first_score
first_score = 95
print(first_score)
print(second_score)95
88second_score stayed at 88 because the third line never touched the value 88 at all. It moved the
name first_score onto a different value. A name can be moved; the number itself cannot be changed.
Naming Rules and Conventions
Python enforces a short list of hard rules about what a name may look like. A name may contain letters,
digits, and underscores. It may not start with a digit, and it may not contain a space, a hyphen, or
punctuation. Names are case-sensitive, meaning score and Score are two completely different
variables. Finally, a name cannot be one of Python's keywords — the roughly three dozen words such
as if, for, class, and return that the language has reserved for its own grammar.
You do not have to memorise that list. Python ships with two tools that answer the question directly:
the string method .isidentifier() reports whether some text is shaped like a legal name, and the
keyword module's iskeyword() function reports whether it is reserved.
import keyword
print("user_name".isidentifier())
print("2fast".isidentifier())
print("user name".isidentifier())
print("class".isidentifier())
print(keyword.iskeyword("class"))True
False
False
True
TrueRead that last pair carefully. "class" is shaped like a legal name, so .isidentifier() says True,
but iskeyword() also says True — and being a keyword is what disqualifies it. Writing class = 5
is a syntax error, meaning Python refuses to run the file at all.
Case sensitivity trips up beginners constantly, so it is worth seeing once:
score = 10
Score = 99
print(score)
print(Score)10
99Beyond the rules, there are conventions — habits that are legal either way but that every Python
programmer follows so that code reads consistently. The main one is snake_case: write names in
lowercase with underscores between words. The second is to describe the value, not its type. A name
like n or x2 forces a reader to scroll around hunting for what it means, while a descriptive name
answers the question on the spot:
first_name = "Ada"
items_in_cart = 3
is_logged_in = True
print(first_name, items_in_cart, is_logged_in)Ada 3 TrueNotice that print() accepts several values separated by commas and joins them with a single space.
Notice too that a variable holding a true-or-false answer is conventionally named as a question the
answer fits — is_logged_in rather than login.
The Five Types You Meet First
Every value in Python has a type, which determines what you can do with it. Five built-in types cover
almost everything a beginner writes, and the built-in type() function will tell you which one you are
holding at any moment:
count = 7
price = 4.99
name = "Ada"
is_ready = True
result = None
print(type(count))
print(type(price))
print(type(name))
print(type(is_ready))
print(type(result))<class 'int'>
<class 'float'>
<class 'str'>
<class 'bool'>
<class 'NoneType'>The word class in that output is Python's internal word for "type" — read <class 'int'> as simply
"an int".
int is a whole number, positive or negative, with no decimal point: 7, 0, -40. Unlike many
languages, Python places no upper limit on how large an integer can get. float is a number with a
decimal point: 4.99, -0.5, 3.0. The name is short for "floating point", which refers to how the
decimal point can move to represent both tiny and enormous numbers.
Arithmetic mixes the two in a way that surprises people, because the plain division operator always produces a float — even when the division comes out even:
print(7 + 3)
print(7 / 2)
print(7 // 2)
print(7 % 2)
print(type(6 / 3))10
3.5
3
1
<class 'float'>/ is true division and always returns a float. // is floor division, which divides and then discards
anything after the decimal point, keeping the result an int. % is the modulo operator, which gives the
remainder — 7 % 2 is 1 because 2 goes into 7 three times with 1 left over.
Floats carry one important warning. They are stored in binary, and some ordinary decimal fractions have no exact binary form, in the same way that one third has no exact decimal form. The result is small errors that show up in unexpected places:
print(0.1 + 0.2)
print(0.1 + 0.2 == 0.3)
print(round(0.1 + 0.2, 2) == 0.3)0.30000000000000004
False
TrueThis is not a bug in Python; every language using standard floating-point numbers behaves this way. The practical lesson is to avoid testing two floats for exact equality, and to round before comparing.
str is a string: text, written inside single or double quotes. Python treats both quote styles
identically, so pick one and stay consistent. Strings can be joined with + and repeated with *, and
len() reports how many characters one holds:
single = 'Ada'
double = "Ada"
print(single == double)
print(len("Lovelace"))
print("Ada" + " " + "Lovelace")
print("ha" * 3)True
8
Ada Lovelace
hahahabool holds exactly one of two values, written True and False with a capital first letter. Every
comparison you write produces a bool, which is what makes bools the fuel for if statements. Other
values can be interpreted as true or false too: bool() treats zero, the empty string, and None as
false, and treats essentially everything else as true:
is_weekend = False
print(is_weekend)
print(type(is_weekend))
print(3 > 2)
print(bool(0), bool(42), bool(""), bool("no"))False
<class 'bool'>
True
False True False TrueThat final True catches people out: bool("no") is true because the string is not empty. Python is
looking at whether text exists, not at what the text says.
NoneType is the type of a single special value, None, which represents the deliberate absence of
a value. It is not zero and not an empty string — it is Python's way of saying "nothing here yet". You
will meet it most often as the result of a function that prints something but never returns anything:
def greet(person):
print("Hi, " + person)
outcome = greet("Ada")
print(outcome)Hi, Ada
NoneTest for it with is None rather than == None. The is operator asks whether two names point at the
very same object, and since there is only ever one None in a running program, that is the precise
question you want to ask.
Converting Between Types
A value's type is fixed, but you can build a new value of a different type from it. The three
conversion functions you will reach for constantly are int(), float(), and str(). Each one leaves
its input untouched and hands back a fresh value:
age_text = "34"
age = int(age_text)
print(age + 1)
print(type(age))
print(type(age_text))35
<class 'int'>
<class 'str'>age_text is still the string "34" afterwards; int() did not transform it in place. This matters
because converting text to a number is the single most common conversion in real programs — anything a
user types, and anything read from a file, arrives as a string.
Converting a float to an int truncates rather than rounds, chopping off everything after the decimal
point and moving toward zero. When you actually want rounding, use round():
print(float("2.5") + 0.5)
print(int(9.99))
print(int(-9.99))
print(round(9.99))
print(str(12) + " birds")3.0
9
-9
10
12 birdsConversions fail when the text does not describe a number of the requested kind, and the failure is a
ValueError. You can catch one with try and except so your program reports the problem instead of
stopping:
raw = "twelve"
try:
number = int(raw)
print(number)
except ValueError as error:
print("Could not convert:", error)Could not convert: invalid literal for int() with base 10: 'twelve'One failure case genuinely catches everyone: int() rejects a string containing a decimal point, even
though the text plainly describes a number. int() parses whole numbers only. The fix is to go through
float() first and then truncate. Surrounding spaces, on the other hand, are forgiven:
try:
print(int("3.9"))
except ValueError as error:
print("int() refused:", error)
print(int(float("3.9")))
print(int(" 42 "))int() refused: invalid literal for int() with base 10: '3.9'
3
42Building Readable Output with f-strings
Stitching values into a sentence with + forces you to convert every number by hand, and the quotes and
plus signs quickly outnumber the words. An f-string solves this. Put the letter f immediately
before the opening quote, and then anywhere inside the text you can write a pair of curly braces with an
expression in them. Python evaluates the expression and drops the result straight into the string,
converting it to text for you:
apples = 4
print("I have " + str(apples) + " apples")
print(f"I have {apples} apples")I have 4 apples
I have 4 applesThe braces can hold any expression, not just a bare variable name, and a colon after the expression
introduces a format specification that controls how the value is displayed. The most useful one for
a beginner is .2f, which shows a float with exactly two digits after the decimal point:
name = "Ada"
score = 91.4567
attempts = 3
print(f"{name} scored {score} points.")
print(f"{name} scored {score:.2f} points over {attempts} attempts.")
print(f"Average per attempt: {score / attempts:.2f}")
print(f"{attempts=}")Ada scored 91.4567 points.
Ada scored 91.46 points over 3 attempts.
Average per attempt: 30.49
attempts=3That last line uses a debugging shortcut: an equals sign just before the closing brace prints the expression text along with its value, which saves typing when you are checking what a variable currently holds.
Python Is Dynamically Typed
In some languages you must declare in advance that a variable holds an integer, and it holds integers forever. Python is dynamically typed: types belong to values, not to names, so a name is free to be rebound to a value of an entirely different type at any moment.
box = 42
print(type(box))
box = "forty-two"
print(type(box))
box = None
print(type(box))<class 'int'>
<class 'str'>
<class 'NoneType'>This flexibility is convenient, but it moves a burden onto you: nothing warns you when a variable is
carrying a different type than you assumed. The classic case is text that looks like a number. The
string "3" and the integer 3 are different values, and the operators treat them completely
differently:
quantity = "3"
print(quantity * 2)
print(int(quantity) * 2)
print(type(quantity * 2))33
6
<class 'str'>"3" * 2 repeated the text and produced "33", not 6. Nothing crashed and nothing was flagged — the
program simply computed a wrong answer and carried on. This is exactly why the built-in input()
function deserves respect: it always returns a string, no matter how number-like what the user typed
looks. Convert it the moment you receive it, and your later arithmetic will behave.
Two habits keep dynamic typing from biting you. First, convert values at the edges of your program, as
soon as data arrives, rather than scattering int() calls throughout. Second, reach for type() the
instant a result looks wrong; it takes one line and usually answers the question immediately.
A Worked Example
Here is a small, complete program that calculates a receipt line. It brings together assignment,
descriptive snake_case names, None, conversion from text, a bool, and f-string formatting.
item_name = "Notebook"
unit_price_text = "3.75"
quantity_text = "4"
discount_code = None
unit_price = float(unit_price_text)
quantity = int(quantity_text)
subtotal = unit_price * quantity
has_discount = discount_code is not None
if has_discount:
total = subtotal * 0.9
else:
total = subtotal
print(f"Item: {item_name}")
print(f"Unit price: {unit_price:.2f}")
print(f"Quantity: {quantity} ({type(quantity).__name__})")
print(f"Subtotal: {subtotal:.2f}")
print(f"Discount applied: {has_discount}")
print(f"Total due: {total:.2f}")Item: Notebook
Unit price: 3.75
Quantity: 4 (int)
Subtotal: 15.00
Discount applied: False
Total due: 15.00Walking through it line by line: the first four lines bind names to raw incoming data. Two of them end
in _text deliberately, as a reminder to the reader that those values are strings rather than numbers —
exactly the kind of data a form or a file would hand you. discount_code is bound to None, which says
"no discount was supplied", a statement an empty string could not make as clearly.
The next block converts and calculates. float(unit_price_text) builds the number 3.75 from the text
"3.75", and int(quantity_text) builds 4 from "4". Only after both conversions is it safe to
multiply them, and subtotal becomes 15.0 — a float, because multiplying a float by an int produces a
float. Then discount_code is not None asks whether a code was actually supplied and stores the answer
as a bool in has_discount. Using is not None rather than a comparison is the correct way to test
against None.
The if statement reads that bool and binds total to one of two calculated values. Because assignment
is just name-binding, both branches are free to bind the same name, and whichever branch runs decides
what total refers to afterwards.
The six print() calls build their output with f-strings. {unit_price:.2f} displays 3.75 with two
decimal places, and the same specification turns the float 15.0 into the tidy 15.00 a receipt needs —
the underlying value is unchanged, only its display. {type(quantity).__name__} runs an expression
inside the braces: type(quantity) reports the type, and .__name__ pulls out just the short word
int instead of the longer form you saw earlier. {has_discount} shows that a bool prints as False
with no conversion needed.
Common Mistakes
Confusing = with ==. A single equals sign performs assignment — it changes what a name points
at. A double equals sign performs comparison — it asks a question and produces a bool. Writing
if count = 5: is a syntax error, and Python refuses to run the file:
count = 5
# if count = 5: <- a syntax error; Python rejects this line
if count == 5:
print("count is five")
count = 6
print(count == 5)count is five
FalseThe comparison on the last line returns False without altering count, which is the whole point of
the distinction: == reads the variable, = rewrites it. When you get a syntax error pointing at an
if or while line, this is the first thing to check.
Joining a string and an int with +. The + operator adds two numbers or concatenates two strings,
but it refuses to guess what you meant when given one of each, raising a TypeError:
age = 30
try:
print("I am " + age + " years old")
except TypeError as error:
print("TypeError:", error)
print("I am " + str(age) + " years old")
print(f"I am {age} years old")TypeError: can only concatenate str (not "int") to str
I am 30 years old
I am 30 years oldThere are two fixes. Convert the number explicitly with str(), or use an f-string, which converts for
you. The f-string version is shorter and far easier to read, and it is what you should reach for by
default.
Calling int() on text that is not a whole number. Any stray character — a unit, a currency symbol,
a comma, a stray letter — makes the conversion raise a ValueError. Since the text often comes from a
person, you cannot assume it will be clean:
user_entry = "42kg"
try:
print(int(user_entry))
except ValueError as error:
print("ValueError:", error)
if user_entry.isdigit():
print(int(user_entry))
else:
print(f"'{user_entry}' is not a whole number, so nothing was converted")ValueError: invalid literal for int() with base 10: '42kg'
'42kg' is not a whole number, so nothing was convertedThe fix is to check before you convert, or to catch the ValueError and respond sensibly. The string
method .isdigit() returns True only when every character is a digit, which makes it a quick guard for
positive whole numbers — though be aware it also rejects "-5", because a minus sign is not a digit.
Catching the ValueError is the more complete approach. Never assume a conversion will succeed just
because the value looks numeric to you.
Using a built-in name as your own variable. Names such as str, list, sum, type, and input
already refer to Python's built-in tools. Binding one of them to your own value hides the original for
the rest of the program, and the error you eventually get looks unrelated to the line that caused it:
str = "shadowed"
try:
print(str(99))
except TypeError as error:
print("TypeError:", error)
del str
print(str(99))TypeError: 'str' object is not callable
99The message says the string is "not callable" because str(99) tried to call a piece of text as though
it were a function. Here del str removes the shadowing name so the built-in becomes reachable again,
but the real fix is to never take the name in the first place — use text or label instead of str,
and items instead of list.
Misspelling a variable name. Python creates a variable the instant you assign to it, so a typo on
the left of an = silently creates a second, unrelated variable, while a typo anywhere you read the
value raises a NameError:
total_price = 19.99
try:
print(total_prise)
except NameError as error:
print("NameError:", error)
print(total_price)NameError: name 'total_prise' is not defined
19.99Read the quoted name in the error message carefully — it is telling you the exact spelling Python looked for and could not find. Comparing it against the name you meant usually reveals the typo immediately. Short, consistent snake_case names reduce how often this happens at all.
Next Steps
You now have the two building blocks every Python program is assembled from: names bound to values, and
values that carry a type. The Two Number Sum practice problem is the natural place to apply them —
it hands you numbers, asks you to combine them toward a target, and rewards clear variable names and a
firm grasp of when a value is an int rather than a string. Before you attempt it, open the Python
playground and retype a few examples from this lesson, changing the values as you go. Try calling
type() on a result that surprises you, and try converting a string that cannot become a number, so
that you recognise the ValueError on sight when it turns up in your own code.
Write it yourself
Reading about code and writing it are different skills. These exercises practise exactly what this lesson covered; they run in this tab and need no account.
When you want a harder one
Interview-style problems graded against hidden tests — a big step up from the exercises. Come back to these when the ideas in this lesson feel comfortable rather than new.