← Back to Core Python
Lesson 3 · Python Foundations

Python Data Types

int, float, complex, bool, str, NoneType - and checking which is which.

Beginner30 min

What you will be able to do

  • Say what a data type is, and why the same operator behaves differently on different types
  • Recognise int, float, complex, bool, str, and NoneType
  • Check a type with type() and test membership with isinstance()
  • Convert between types, and predict which conversions fail
  • Explain why 0.1 + 0.2 is not 0.3, and what to use when it matters
  • Tell None apart from 0 and from the empty string
  • Say at a high level which built-in types can be changed in place

The idea, in plain English

A data type describes what kind of thing an object is, and therefore what you can do with it. That second half is the useful part: `+` joins two strings and adds two numbers, and it is the types of the objects - not the operator - that decide which happens.

Following on from the last lesson: the type belongs to the object, not to the name. `age = 34` does not make `age` an integer variable. It points the name at an integer object, and pointing it at a string tomorrow is perfectly legal.

Six types cover almost everything you will write at this stage. Four are numbers or near enough - int, float, complex, bool - one is text, and one, None, exists to represent the absence of a value. Collections come later.

The single most common bug for beginners lives here: anything arriving from a form, an API, a CSV, or the command line arrives as text. `"100"` looks like a number and is not one, and the moment you try to do arithmetic with it Python will tell you so.

Worked example: Turning "1500" and "3" from a form into a total of 4500.

The same characters, two different things

This is the confusion that costs beginners the most time, and it is worth being blunt about: "100" and 100 are unrelated objects that happen to print the same way. Every operator below behaves differently depending on which one it is given.

What makes it dangerous is that most of these run without raising anything. "100" * 2 is not an error - it is a perfectly good string repetition, and a perfectly wrong total.

The same expression, both ways
+"100" + "1" joins text into "1001". 100 + 1 adds to 101. Mixing them raises TypeError.
*"100" * 2 repeats to "100100". 100 * 2 multiplies to 200. No error either way.
>"9" > "100" is True - strings compare character by character. 9 > 100 is False.
len()len("100") is 3. len(100) raises TypeError - a number has no length.
sum()Over strings it fails. Over ints it does what you meant.

Tip: Convert at the boundary. The moment data arrives from a form, an API, or a file, turn it into the type you want - then nothing downstream has to wonder.

What int() does to a string

Three outcomes, and only one of them is the one you were picturing. The third is the one that does damage quietly.

int() on a string
Worksint("100") is 100. Clean digits, optional sign, nothing else.
Raisesint("12.5"), int("hello"), int("1,000"), int("") all raise ValueError. int() will not guess.
Truncatesint(float("12.9")) is 12, not 13. int() cuts towards zero rather than rounding, so int(-12.9) is -12. Use round() when you want rounding.

Watch out: If the string came from a user, a file, or an API, it is not yours to trust. Wrap the conversion in try/except and decide what a bad value should do.

Why 0.1 + 0.2 is not 0.3

Run `0.1 + 0.2` and Python prints 0.30000000000000004. Nothing is broken. A float stores a number in binary, and 0.1 has no exact binary representation - in the same way one third has no exact decimal one. The tiny error is in the storage, and arithmetic makes it visible.

This matters the moment money is involved. `0.1 + 0.2 == 0.3` is False, so a total that should match a price will not, and a balance check can fail for a customer who paid exactly the right amount.

For money, use decimal.Decimal and build it from a string - Decimal("0.1"), not Decimal(0.1), because the second one inherits the float error you were trying to escape. For comparing measurements, compare within a tolerance rather than with ==.

Watch out: Never use == between two floats you arrived at by different routes. Use math.isclose(a, b), or work in integer units - store paise or cents rather than rupees or dollars.

None is not zero, and not empty

None is its own type - NoneType - with exactly one value. It means "there is no value here", which is a different statement from "the value is zero" or "the value is an empty string".

The distinction is not academic once you are reading from a database or an API. A middle_name of None means the field was never filled in; a middle_name of "" means someone submitted the form with it blank. A discount of None means no discount applies; a discount of 0 means one applies and it is worth nothing.

Test for it with `is None` rather than `== None`. `is` asks whether it is that exact object, and since there is only ever one None, that is both faster and impossible for a class to override.

Tip: None, 0, and "" are all falsy, so `if not value:` treats them identically. When the difference matters, say `if value is None:` and mean it.

Syntax and examples

The six types you will use first
count = 34 # int - whole numbers price = 99.99 # float - decimals signal = 2 + 3j # complex - real + imaginary is_active = True # bool - True / False name = "Chandu" # str - text middle_name = None # NoneType - no value at all for value in (count, price, signal, is_active, name, middle_name): print(value, "->", type(value))
Two division operators, two types back
a, b = 20, 6 print(a / b) # 3.3333333333333335 true division, always float print(a // b) # 3 floor division, stays int print(a % b) # 2 remainder print(a ** b) # 64000000 power # Integers have no fixed size - this is a normal int, not a special type print(999999999999999999999999999999999999 + 1)
The float that is not quite right
print(0.1 + 0.2) # 0.30000000000000004 print(0.1 + 0.2 == 0.3) # False from decimal import Decimal print(Decimal("0.1") + Decimal("0.2")) # 0.3 print(Decimal("0.1") + Decimal("0.2") == Decimal("0.3")) # True # Decimal(0.1) would carry the float error in - always build from a string.
bool is a kind of int
print(int(True), int(False)) # 1 0 print(True + True) # 2 - legal, and rarely what you meant print(isinstance(True, int)) # True - bool is a subclass of int # Which makes this idiom work: results = [True, False, True, True] print(sum(results)) # 3 - counts the Trues
None against its lookalikes
missing = None zero = 0 empty = "" print(missing == zero) # False print(missing == empty) # False print(type(missing)) # <class 'NoneType'> # The right way to test for it if missing is None: print("no value supplied")
Converting form data into numbers
product_price = "1500" # everything arrives as text quantity = "3" total = int(product_price) * int(quantity) print(total) # 4500 # Untrusted input needs a decision about failure raw = "not a number" try: value = int(raw) except ValueError: value = 0 # or re-raise, or ask again print(value) # 0

Tip: A phone number is a string, not an int. Leading zeros survive, +91 is allowed, and you are never going to add two phone numbers together. The same applies to postcodes, PAN numbers, and order references.

The built-in types in this lesson

Six types, and what each one is for.

int

Whole numbers, positive or negative, with no size limit beyond memory.

count = 34
float

Numbers with a decimal part, stored in binary - so approximate.

price = 99.99
complex

A real and an imaginary part. Common in engineering and signal processing, rare in backend work.

z = 2 + 3j   # z.real, z.imag
bool

True or False. Technically a subclass of int, where True is 1 and False is 0.

is_active = True
str

Text. Single or double quotes, and it cannot be changed in place once made.

name = "Chandu"
NoneType

The type of None, the single object meaning "no value". Test with `is None`.

middle_name = None

Can it be changed in place?

Immutable means the object itself can never change - reassigning a name just points it elsewhere. This is the distinction behind the aliasing surprise from the last lesson, and it decides which types can be dictionary keys.

int

Immutable

float

Immutable

complex

Immutable

bool

Immutable

str

Immutable - "abc"[0] = "x" is an error

NoneType

Immutable

tuple

Immutable

list

Mutable - append, remove, and assignment by index all change the object

dict

Mutable

set

Mutable

Try it yourself

The code does not change. Swap the content string and the program does something else entirely.

The classic

“print(0.1 + 0.2, 0.1 + 0.2 == 0.3)”

String repetition

“print("100" * 2, 100 * 2)”

Truncation, not rounding

“print(int(12.9), round(12.9))”

bool really is an int

“print(True + True, isinstance(True, int))”

What usually goes wrong

Treating "100" as a number

The worst part is that it often does not raise. "100" * 2 gives "100100" and "100" + "1" gives "1001" - both run happily and both are wrong. Convert at the edge, where the data arrives.

✗ total = price * quantity      # both are str
✓ total = int(price) * int(quantity)
Assuming every string converts

int() raises ValueError on "12.5", "hello", "1,000", and an empty string. If the value came from a user, a file, or an API, handle the failure rather than hoping.

✗ age = int(request_value)
✓ try:
    age = int(request_value)
except ValueError:
    age = None
Expecting int() to round

int(12.9) is 12. It truncates towards zero rather than rounding, so int(-12.9) is -12 and not -13. Use round() when you want rounding.

✗ score = int(12.9)       # 12
✓ score = round(12.9)     # 13
Comparing floats with ==

Two floats that should be equal often are not, because the binary representation carries a tiny error. This bites hardest in money and in test assertions.

✗ if total == 0.3:
✓ import math
if math.isclose(total, 0.3):
Treating None as 0 or ""

They are all falsy, so `if not value:` cannot tell them apart. When "not supplied" and "supplied as empty" mean different things - and in an API they usually do - check for None explicitly.

✗ if not discount:        # true for None AND for 0
✓ if discount is None:    # only true for None

Best practices

  • Convert incoming data to the type you want at the boundary, then trust it everywhere inside.
  • Use isinstance() rather than type(x) == int for checks - it handles subclasses, which is why isinstance(True, int) is True.
  • Use decimal.Decimal for money, built from strings, and never float.
  • Check for None with `is None`, not `== None`.
  • Store identifiers as strings whenever leading zeros, symbols, or formatting matter - phone numbers, postcodes, account numbers.
  • Prefer // when you want an integer result, rather than converting after / and hoping.

Practice

Write these yourself before opening anything. Getting them wrong first is most of how this sticks.

1.

Create one variable holding each of 25, 25.5, "25", True, None, and 2 + 3j, then print the type of each.

Show hint

A tuple and a for loop saves writing six print calls.

Show solution
values = (25, 25.5, "25", True, None, 2 + 3j) for value in values: print(value, "->", type(value))
2.

Convert "100" to an int, "99.99" to a float, 100 to a str, and 1 to a bool. Print each result and its type.

Show solution
print(int("100"), type(int("100"))) print(float("99.99"), type(float("99.99"))) print(str(100), type(str(100))) print(bool(1), type(bool(1)))
3.

Build a student record with a name, age, percentage, a pass flag, and remarks that have not been written yet. Print each value with its type.

Show hint

Remarks that do not exist yet is what None is for.

Show solution
name = "Rahul" age = 22 percentage = 87.5 is_passed = True remarks = None for value in (name, age, percentage, is_passed, remarks): print(value, "->", type(value))
4.

A form sends product_price = "1500" and quantity = "3". Work out the total, which should be 4500 as a number.

Show hint

Convert both before multiplying. If you multiply first you get a very long string.

Show solution
product_price = "1500" quantity = "3" total = int(product_price) * int(quantity) print(total) # 4500 print(type(total)) # <class 'int'>
Coding challenge

A user registration record

Model one registered user with the right type for every field, then print each value alongside its type. The interesting decision is the phone number.

It should
  • Store a name, age, email, verification flag, and phone number
  • Choose the type for each field deliberately rather than by habit
  • Store the phone number as a string, and write a comment saying why
  • Include one field that has not been supplied yet, using None
  • Print every field with its value and its type
Start here
name = "" age = 0 email = "" is_verified = False phone_number = "" # print each field with its type
Show one solution
One solution
name = "Rahul Sharma" age = 28 email = "rahul@example.com" is_verified = True # A string, not an int: an int drops the leading zero, cannot hold "+91", # and there is no arithmetic you would ever want to do with it. phone_number = "+91 09876 54321" profile_image = None # not uploaded yet fields = { "name": name, "age": age, "email": email, "is_verified": is_verified, "phone_number": phone_number, "profile_image": profile_image, } for label, value in fields.items(): print(f"{label:14} {str(value):22} {type(value).__name__}")

Key points

  • A data type says what an object is and what can be done with it - the same operator behaves differently on different types.
  • The type belongs to the object, not to the name.
  • int, float, complex, bool, str, and NoneType cover almost everything before collections.
  • Python integers have no fixed size; floats are binary approximations.
  • 0.1 + 0.2 != 0.3. Use decimal.Decimal built from strings for money, and math.isclose for comparisons.
  • bool is a subclass of int: True is 1, False is 0, and sum() over booleans counts them.
  • Everything from a form, an API, a CSV, or the command line arrives as str.
  • int() truncates towards zero rather than rounding, and raises ValueError on anything it cannot read.
  • None is its own type meaning "no value" - not 0, not "". Test it with `is None`.
  • int, float, bool, str, and tuple cannot be changed in place; list, dict, and set can.

Quick check before you move on

What does this print? value = "100" print(type(value)) value = int(value) print(type(value)) value = 10.5 print(type(value))
<class 'str'>, then <class 'int'>, then <class 'float'>. The name is rebound each time; the type shown is always the type of whatever object it currently refers to.
Why is 0.1 + 0.2 == 0.3 False?
Because 0.1 and 0.2 have no exact binary representation, so each is stored with a tiny error and the sum carries both. The result is 0.30000000000000004, which is genuinely not equal to 0.3.
What is the difference between None, 0, and ""?
None means no value exists. 0 is a number. "" is a string of zero characters. All three are falsy, so only an explicit `is None` can tell the first apart from the others.
Why store a phone number as a string?
An int loses leading zeros, cannot hold a + or spaces, and could overflow a database column type. And there is no arithmetic you would ever perform on one.

Interview questions

Why is 0.1 + 0.2 not equal to 0.3, and what do you do about it?

Floats follow IEEE 754 binary representation, and 0.1 has no finite binary form - just as a third has no finite decimal one. Each operand carries a small error and arithmetic exposes it. For money use decimal.Decimal constructed from strings, or work in the smallest integer unit such as cents. For general comparison use math.isclose rather than ==.

Is bool a separate type from int?

It is its own type but a subclass of int, so True == 1, True + True == 2, and isinstance(True, int) is True. That inheritance is useful - sum() over a list of booleans counts them - and occasionally surprising, such as when a dict treats the keys 1 and True as the same key.

When would you use type() rather than isinstance()?

Almost never in application code. isinstance() accepts subclasses, which is usually what you want. type(x) is Y is for the rare case where you need the exact type and must exclude subclasses - for example rejecting a bool where a genuine int is required.

What is the difference between None and a missing key?

None is a value meaning "no value". A missing key means the entry does not exist at all. d.get("k") returns None for both, which hides the distinction - "k" in d answers it directly. This matters for APIs, where a field sent as null and a field omitted entirely often have different meanings.

Why does Python not have int32 or int64?

Python integers are arbitrary precision - they grow as needed and are limited only by memory, so overflow simply does not occur. The cost is that they are objects rather than machine words, which makes them slower and larger; libraries like NumPy provide fixed-width types when that cost matters.

What does immutable actually guarantee?

That the object itself can never change after creation. It does not mean the name cannot be rebound - s = s + "x" is fine, it just builds a new string. The guarantee is what makes immutable types safe as dictionary keys and safe to share between threads without locking.

Quiz

  1. 1.

    What are the six fundamental built-in types covered here?

  2. 2.

    What is the difference between "100" and 100?

  3. 3.

    What does int("12.5") do, and what about int(float("12.5"))?

  4. 4.

    Is True equal to 1?

  5. 5.

    Why check `value is None` rather than `value == None`?

  6. 6.

    Which of int, str, list, tuple, dict can be changed in place?

Comments

Sign in to leave a comment. Your name and photo come from Google; nothing else is shared.

Loading comments...