2  Objects and Variables

Python programs operate on different kinds of objects. An object is a fundamental concept in Python - it’s a self-contained unit that combines both data (values) and the operations that can be performed on that data. Every value in Python is an object, whether it’s a simple number, a piece of text, or something more complex.

We can think of an object as a region of memory whose contents represent a value of a certain kind. For example, an integer object representing the value 232 is stored in memory as a pattern of binary digits (zeros and ones). A string object would have a more complex memory representation. However, as Python programmers, we don’t need to worry about these internal representations. Instead, we focus on:

  1. What kind of data the object holds (its type)
  2. What operations we can perform on it (its methods and attributes)
  3. How we can use it in our programs

The language provides us with certain basic kinds of objects (objects of primitive types) that serve as building blocks. We can then make more complicated objects by combining these primitives in various ways. This object-based approach gives Python programs their flexibility and power.

But before we start looking at the different kind of objects that Python deal with, lets look at the way Python gives names to objects: variables.

2.1 Variables and Object Binding

In Python, variables are names that bind to objects. When you create a variable, you’re creating a name that refers to an object in memory. This binding is established using the assignment operator =. For example:

x = 42  # Creates a binding between name 'x' and integer object 42
message = "Hello"  # Binds 'message' to string object "Hello"

Variables can be used anywhere their bound objects could be used. For instance:

number = 5
result = abs(-number)  # Using variable 'number' with built-in abs() function
print(result)  # Outputs: 5

text = "hello"
upper_text = text.upper()  # Using variable with string method
print(upper_text)  # Outputs: HELLO

A key feature of Python variables is that they can be rebound to different objects at any time:

x = 42          # x binds to integer 42
x = "Hello"     # x now binds to string "Hello"
x = 3.14        # x now binds to float object

Multiple variables can bind to the same object:

a = "Hello"     # Creates a string object and binds 'a' to it
b = a          # Binds 'b' to the same string object
print(len(a))  # Outputs: 5
print(len(b))  # Outputs: 5 (both reference the same string)

Assignment creates new bindings but doesn’t copy objects. When working with primitive types like numbers and strings, this distinction is less important since they are immutable - their values cannot be changed after creation.

2.1.1 Variable Names and Naming Conventions

Variable names in Python must follow certain rules:

  • Names must start with a letter (a-z, A-Z) or underscore (_)
  • After the first character, names can contain letters, numbers, and underscores
  • Names are case-sensitive (temperature and Temperature are different variables)
  • Names cannot be Python keywords like if, for, while, True, etc.
# Valid variable names
temperature = 98.6
_hidden = "secret"
counter2 = 42
first_name = "Alice"

# Invalid variable names
2counter = 42     # Cannot start with number
my-value = 10     # Cannot use hyphen
class = "Math"    # Cannot use Python keyword

Python follows these naming conventions (PEP 8):

  • Use lowercase letters for most variable names: temperature, count, name
  • Use underscores to separate words in longer names: first_name, room_temperature
  • Start with underscore for non-public variables: _internal_value
  • Use meaningful names that describe the data: temperature rather than t

Good variable naming practices:

  • Choose descriptive names that indicate the variable’s purpose
  • Use full words rather than abbreviations (except for common ones)
  • Make names as long as needed to be clear, but not longer
# Good variable names
temperature = 98.6
first_name = "Alice"
room_count = 5

# Poor variable names (avoid these)
t = 98.6          # Too short, unclear meaning
fn = "Alice"      # Unclear abbreviation
x = 5             # Uninformative name

2.2 Primitive object types and operations on them

Python is a richly-typed language, meaning it comes with many different kinds of objects to represent different types of data and operations. While programmers can create their own custom object types (which we’ll explore later), Python provides a set of fundamental building blocks called primitive types. These are the basic data types built into the language itself, like numbers and strings.

Think of primitive types as the atoms from which we build more complex molecules. Just as all matter is made of fundamental particles, all Python programs are built using these primitive types, either directly or by combining them into more complex structures. Understanding these primitive types is essential because:

  1. They represent the basic kinds of data most programs need to work with
  2. They provide the foundation for building more complex data structures
  3. They implement the fundamental operations that more complex objects build upon

2.2.1 Number types

Python supports integers, which are whole numbers like 3, -7, or 2024. Operations on integers include addition (3 + 2), subtraction (10 - 4), multiplication (6 * 7), and division (8 / 2).

The exponentiation operator (**) allows you to raise a number to a power. For example, 2 ** 3 evaluates to 8 (2 raised to the power of 3). The modulo operator (%) computes the remainder of division. For instance, 10 % 3 results in 1, as 10 divided by 3 leaves a remainder of 1.

Floating point numbers represent real numbers and can include decimals, such as 3.14, -0.001, or 2.0. They support similar operations to integers but are subject to precision limitations. For example, 0.1 + 0.2 may not precisely equal 0.3 due to floating-point representation. Special values like Inf (infinity) and NaN (not a number) exist to handle certain mathematical scenarios. Here are some examples of operations that produce these values:

# Division by zero resulting in infinity
(1.0 / 0.0)  # Results in inf

# Negative division by zero resulting in negative infinity
(-1.0 / 0.0)  # Results in -inf

# Exponentiation producing NaN
(0.0 ** -1)  # Results in nan

# Invalid operations also producing NaN
float('nan')  # Results in nan

# Negative division by zero resulting in negative infinity
  # Outputs: -inf

For advanced mathematical operations, Python provides the math library, which you can import using import math.

2.2.2 Importing Libraries and the Math Library

Python includes a vast collection of additional functionality bundled into packages, which are collections of related modules. A module is a single Python file containing code (such as functions and classes) that provides specific functionality. A package is a higher-level structure—a collection of related modules grouped together to organize code. When working with Python’s standard library or third-party libraries, you will often encounter both terms.

When you import a module, Python creates a namespace for that module. A namespace is a container that holds the module’s functions, variables, and classes, ensuring they do not conflict with names in other parts of your program. Modules provide namespaces, and packages allow for hierarchical organization of these modules into logical groupings.

For example, the math module provides mathematical functions and constants, accessible through its namespace, using the . syntax.

Here is how it works:

import math

# Using functions from the math library
print(math.sqrt(16))  # Outputs: 4.0
print(math.pi)        # Outputs: 3.141592653589793
print(math.sin(math.pi / 2))  # Outputs: 1.0

The above example demonstrates key aspects of working with the math module:

  • The . syntax is used to access attributes or functions within a module. For example, math.sqrt refers to the sqrt function (square root) in the math namespace.
  • math.pi is a constant provided by the module, representing the mathematical constant π (pi).
  • math.sqrt and other functions in the module are invoked using parentheses, following the same logic as mathematical functions, e.g., sqrt(x).
  • Comments in Python, indicated by #, are used to annotate the code and explain its purpose, helping improve readability and maintainability. The rest of the line after the # is ignored by the interpreter.

The import statement can also be tailored. You can import specific functions or variables, like this:

from math import sqrt, pi

print(sqrt(25))  # Outputs: 5.0
print(pi)        # Outputs: 3.141592653589793

Alternatively, you can alias imports for brevity:

import math as m

print(m.cos(0))  # Outputs: 1.0
print(m.e)       # Outputs: 2.718281828459045

The math library provides a variety of functions beyond basic arithmetic, such as trigonometric functions (sin, cos), logarithms (log), and constants like e and pi. By leveraging the standard library, you can perform complex calculations efficiently and effectively.

2.2.3 Unicode strings

Unicode is a standard for encoding text in various writing systems, covering diverse writing systems including most human scripts, mathematical symbols, and even emojis. In Python, strings are Unicode by default. You can define strings using single quotes ('example'), double quotes ("example"), or triple quotes ('''example''' or """example"""). Triple quotes allow strings to span multiple lines, making them useful for multi-line text or documentation within the code. Here is an example of a multi-line string:

"""This is an example of a 
multi-line string. It spans 
several lines."""

"""This is an example of a 
multi-line string. It spans 
several lines."""

You can mix quote types to include one kind of quote inside another, like 'She said, "Hello!"'.

For example, Unicode allows for strings such as "你好" (Chinese), "مرحبا" (Arabic), "🙂" (emoji), and "αβγ" (Greek).

Concatenation refers to the operation of joining two or more strings together. In Python, this is achieved using the + operator. For example, 'Hello' + ' World' results in 'Hello World'. This operation creates a new string by appending the contents of the second string to the first.

2.2.3.1 String Methods

Methods are functions that belong to and operate on specific types of objects. They are a key part of object-oriented programming, where data (the object) and the operations that can be performed on that data (the methods) are bundled together. Methods are accessed using the . syntax and are applied directly to the object. When you call a method on an object, that object is automatically passed to the method as its first argument (often called self in Python).

Here are examples of commonly used string methods:

  • Changing Case: The .upper() and .lower() methods convert strings to uppercase and lowercase, respectively.
"hello".upper()  # Outputs: HELLO
"WORLD".lower()  # Outputs: world
  • Trimming Whitespace: The .strip() method removes whitespace from the beginning and end of a string.
"  text  ".strip()  # Outputs: text
  • Replacing Substrings: The .replace() method replaces occurrences of a substring with another.
"apples and bananas".replace("apples", "oranges")  # Outputs: oranges and bananas

These methods allow for powerful string manipulations, making them essential tools in Python programming.

2.2.4 Booleans

Boolean values, named after mathematician George Boole, represent logical truth values and are either True or False. These values form the foundation of logical decision-making in programming. Just as we use logic in everyday reasoning (“if it’s raining AND I’m going outside, THEN I need an umbrella”), programs use boolean logic to make decisions and control their flow.

The comparison operator == checks for equality, while the single equals sign = is reserved for assignment, which will be covered later. You can obtain boolean values through comparison operators, such as 5 > 3 (True), 4 == 4 (True), and 7 < 2 (False). These comparisons work with numbers, strings, and other types of objects, though the meaning of comparison may vary depending on the type.

Python provides three fundamental boolean operators: - and: Returns True only if both operands are True - or: Returns True if at least one operand is True - not: Reverses the logical value of its operand

These operators follow the classical rules of Boolean logic. When evaluating boolean expressions, Python uses “short-circuit evaluation” - it stops evaluating as soon as the final result is determined. For example, in an and operation, if the first operand is False, Python knows the result must be False regardless of the second operand.

# Short-circuit evaluation example
x = 0
result = x != 0 and (10/x > 1)  # Safely evaluates to False, avoiding division by zero

Python provides a complete set of comparison operators that produce boolean results: - < less than - > greater than - <= less than or equal to - >= greater than or equal to - == equal to - != not equal to

When combining boolean operations, Python follows standard operator precedence: not has the highest precedence, followed by and, then or. Using parentheses can make complex expressions clearer and ensure intended evaluation order.

Complex boolean expressions can be constructed by nesting these operators:

# A nested boolean expression
(5 > 3) and (2 < 4) or not (7 == 8)  # Outputs: True

# Another complex boolean expression
(10 > 5) or ((3 == 2) and not (4 < 1))  # Outputs: True

These concepts are foundational for decision-making in Python programs.

2.3 Working with Dates and Time

Working with dates and times in programming requires specialized tools because these concepts are more complex than they might first appear. Consider these challenges:

  • Dates and times follow irregular patterns (months have different lengths, leap years occur, daylight saving time shifts)
  • Time zones and international date lines make global time coordination complex
  • Many different date formats exist across cultures and regions
  • Date arithmetic isn’t as simple as regular number arithmetic (what’s “one month” after January 31st?)

Python’s datetime module provides comprehensive tools for handling these complexities. The module contains several key classes:

  • datetime: Represents a specific point in time, including date and time
  • date: Represents just a date (year, month, day)
  • time: Represents just a time (hour, minute, second, microsecond)
  • timedelta: Represents a duration or time difference

To work with dates, you first need to import the required classes:

from datetime import datetime, date, timedelta

The datetime class provides several ways to create date objects: - datetime.now(): Creates an object representing the current date and time - datetime(year, month, day, hour, minute): Creates an object for a specific point in time - datetime.strptime(): Creates a datetime object by parsing a string according to a specified format

Time intervals are represented using timedelta objects, which allow you to: - Add or subtract periods of time from dates - Calculate differences between dates - Express durations in various units (days, seconds, microseconds)

Date formatting uses special codes called format directives. Common directives include: - %Y: Four-digit year - %m: Month as a zero-padded number - %d: Day of the month as a zero-padded number - %H: Hour (24-hour clock) - %M: Minute - %S: Second

Here’s a practical example combining these concepts:

from datetime import datetime
current_time = datetime.now()
formatted_date = current_time.strftime('%Y-%m-%d')  # Creates date string like "2024-01-15"

When working with dates, be mindful of: - Time zones (the datetime module includes timezone support) - Date arithmetic across month/year boundaries - Leap years and other calendar complexities

2.4 Interpolating variables in strings

When writing programs, we often need to combine text with the values of our variables to create meaningful messages. Python provides a special kind of string called an f-string (short for “formatted string”) that makes this easy to do.

To create an f-string, you put the letter ‘f’ right before the opening quote of your string. Then, inside the string, you can put variable names or expressions inside curly braces {}. Python will replace these with their actual values. Here’s a simple example:

name = "Alice"
age = 25
# The 'f' before the quotes makes this an f-string
print(f"My name is {name} and I am {age} years old")

When you run this code, Python will: 1. See the ‘f’ before the string and know this is an f-string 2. Look for any curly braces {} in the string 3. Take what’s inside the braces and replace it with its value 4. So {name} becomes “Alice” and {age} becomes “25” The output will be: My name is Alice and I am 25 years old

You can do calculations inside the curly braces too. This is helpful when you need to show results:

price = 49.99
quantity = 3
# The calculation price * quantity happens inside the braces
print(f"Total cost: ${price * quantity}")  # Shows: Total cost: $149.97

You can even use methods (special functions that belong to objects) inside the braces:

greeting = "hello"
# The upper() method converts text to uppercase
print(f"Uppercase greeting: {greeting.upper()}")  # Shows: Uppercase greeting: HELLO

f-strings are the modern and recommended way to put variables into strings in Python. They make your code easier to read because you can see exactly where the variables will go, and they’re less prone to errors than older methods of string formatting.

For numbers, you can control how they’re displayed using a format specifier after a colon inside the braces:

pi = 3.14159
# Show only 2 decimal places using :.2f
print(f"Pi rounded to 2 decimal places: {pi:.2f}")  # Shows: Pi rounded to 2 decimal places: 3.14

Let’s break down the format specifier .2f: - The colon : tells Python that what follows is a format specification - The .2 means “show 2 digits after the decimal point” - The f indicates we want to format this as a floating-point number

This is particularly useful when dealing with money, measurements, or any situation where you want to control how many decimal places to display. For example:

price = 19.99999
print(f"The item costs ${price:.2f}")  # Shows: The item costs $20.00

Here are more examples of f-strings with different format specifications:

# Padding numbers with zeros
account = 12345
print(f"Account #: {account:08d}")  # Shows: Account #: 00012345

# Percentage formatting
ratio = 0.8523
print(f"Progress: {ratio:.1%}")  # Shows: Progress: 85.2%

# Scientific notation
large_num = 1234567.89
print(f"Scientific: {large_num:e}")  # Shows: Scientific: 1.234568e+06

# Aligning text
name = "Bob"
score = 95
print(f"|{name:<10}|{score:>5}|")  # Shows: |Bob       |   95|

# Date formatting
from datetime import datetime
now = datetime.now()
print(f"Date: {now:%Y-%m-%d}")  # Shows current date like: Date: 2025-01-13

2.5 Type and Value Errors

Python will raise errors when operations or functions are used incorrectly. Two common types of errors are TypeError and ValueError.

  • Type Errors: These occur when an operation is applied to an object of an inappropriate type. For example:
# Trying to add a string and a number
"Hello" + 5  # Raises TypeError: can only concatenate str (not "int") to str

# Using a string with a numeric operation
len(10)  # Raises TypeError: object of type 'int' has no len()
  • Value Errors: These occur when a function receives an argument of the correct type but an invalid value. For example:
# Invalid conversion from string to integer
int("abc")  # Raises ValueError: invalid literal for int() with base 10: 'abc'

# Square root of a negative number
import math
math.sqrt(-1)  # Raises ValueError: math domain error

Understanding these errors helps in debugging and writing robust Python programs.

2.6 Exercises

  1. Variable Binding

    What will be printed? Explain why:

    x = 42
    y = x
    x = "Hello"
    print(y)  # What value is y bound to?
  2. String Operations

    Given these strings:

    text1 = "HELLO"
    text2 = "world"
    text3 = "  Python  "

    What will each line print?

    print(text1.lower())
    print(text2.upper())
    print(text3.strip())
  3. Numbers and Math

    What is the result of each calculation?

    x = 10
    y = 3
    
    print(x + y)    # Addition
    print(x - y)    # Subtraction
    print(x * y)    # Multiplication
    print(x / y)    # Division
    print(x ** y)   # Exponentiation
  4. Math Module

    Import math and predict these results:

    import math
    
    print(math.pi)          # What is π?
    print(math.sqrt(25))    # Square root of 25
    print(math.ceil(3.1))   # Ceiling of 3.1
  5. Boolean Values

    What will these print? Why?

    print(5 > 3)           # Greater than
    print(10 == 10)        # Equality
    print(True and False)  # AND operation
    print(True or False)   # OR operation
    print(not True)        # NOT operation
  6. String Formatting

    What will these f-strings display?

    name = "Alice"
    age = 25
    pi = 3.14159
    
    print(f"Name: {name}")
    print(f"Age next year: {age + 1}")
    print(f"Pi to 2 decimals: {pi:.2f}")
  7. Type Checking

    What type will each variable have?

    a = 42
    b = "42"
    c = 3.14
    d = True
    
    print(type(a))
    print(type(b))
    print(type(c))
    print(type(d))
  8. Current Date

    Using datetime, what information will this show?

    from datetime import datetime
    
    now = datetime.now()
    print(f"Year: {now:%Y}")
    print(f"Month: {now:%m}")
    print(f"Day: {now:%d}")