A02 Variables

Problem Without Variables

# Problem without variables
# ------------------------------------------------------------------------------
# This example explains why variables are useful. Without them we would
# repeat the same value throughout the code and update each occurrence
# manually. Imagine printing "Hello world" 1000 times and later wanting
# to change it to "Hello Python"—we would need to edit every place it was
# written.

print("Hello world")
print("Hello world")
print("Hello world")
print("Hello world")
print("Hello world")
# Output: Hello world

Solution With Variables

# Solution: Variables as containers for data
# ------------------------------------------------------------------------------
# This code uses a variable as a container for data. The variable `text`
# stores a string value that can be reused multiple times, illustrating how
# variables allow you to store and manipulate data efficiently.

text = 'Hello world!'
print(text)
print(text)
print(text)
print(text)
print(text)
# Output:
# Hello world!
# Hello world!
# Hello world!
# Hello world!
# Hello world!

Integer Numbers

# Integer numbers
# ------------------------------------------------------------------------------
# The integer type in Python is used to represent whole numbers, both
# positive and negative. Python supports various bases for integers,
# including decimal, binary, octal, and hexadecimal.

decimal_var = 10
print(decimal_var)
# Output: 10

binary_var = 0b1010
print(binary_var)
# Output: 10

octal_var = 0o12
print(octal_var)
# Output: 10

hexadecimal_var = 0x0A
print(hexadecimal_var)
# Output: 10

Real Numbers

# Real numbers
# ------------------------------------------------------------------------------
# Floating point numbers are used to represent real numbers and can be defined
# in standard decimal format or scientific notation.  Python's `float` type is
# based on the IEEE 754 double-precision floating-point format, which provides
# a wide range of values and precision.

float_1 = 1.0       # Standard format for float numbers
float_2 = 1.0e0     # Scientific notation for float numbers (equivalent to 1.0)

print(float_1)
# Output: 1.0

print(float_2)
# Output: 1.0

Complex Numbers

# Complex numbers
# ------------------------------------------------------------------------------
# This code creates and manipulates complex numbers in Python. A complex
# number is written as `a + bj`, where `a` is the real part and `b` is the
# imaginary part.

z1 = 1 + 2j
z2 = 2 + 4j
print(z1, z2)
# Output:
# (1+2j) (2+4j)

Boolean Numbers

# Boolean numbers
# ------------------------------------------------------------------------------
# This example uses boolean variables to represent truth values—either
# `True` or `False`. Such variables are often part of conditional
# statements and logical operations.

var = False
print(var)
# Output:
# False

var = True
print(var)
# Output:
# True

List Data

# List data
# ------------------------------------------------------------------------------
# The list type in Python is used to represent a collection of items. It is
# important to note that lists are passed by reference (see mutability in
# examples). When you assign a list to another variable, both variables point
# to the same list in memory. This means that changes made to one variable
# will affect the other variable as well. To create a copy of a list, you can
# use the `copy()` method or the slicing (e.g. `a = b[:]`). The list has the
# following properties:
#
# - Indexed: Elements can be accessed by their index
# - Mutable: Can be modified after creation
# - Ordered: Order of elements is preserved
# - Heterogeneous: Can contain elements of different types
# - Passed by reference: Changes to one variable affect all references

# Indexed (elements can be accessed by their index)
a = [1, 2, 3]
print(a[0])
# Output: 1

# Mutable (can be modified after creation)
b = [1, 2, 3]
b[0] = 4
print(b)
# Output: [4, 2, 3]

# Ordered (order of elements is preserved)
c = [2, 1, 3]
print(c)
# Output: [2, 1, 3]

# Heterogeneous (can contain elements of different types)
d = [1, 'two', 3.0]
print(d)
# Output: [1, 'two', 3.0]

# Passed by reference (changes to one variable affect all references)
l1 = [1, 2, 3]
l2 = l1
l2[0] = 4
print(l1, l2)
# Output: [4, 2, 3] [4, 2, 3]

Tuple Data

# Tuple data
# ------------------------------------------------------------------------------
# A tuple is very similar to a list that is immutable, meaning it cannot be
# changed after creation. Their main advantage is the memory efficiency
# and performance benefits they provide over lists, especially when dealing
# with large datasets. They also can be used as keys in dictionaries.
#
# - Immutable: Once created, the tuple cannot be changed.
# - Ordered: The elements in the tuple maintain their order.
# - Indexed: Each element in the tuple can be accessed by its index.
# - Heterogeneous: Tuples can contain characters of different types

# Immutable (a change creates a new object)
tuple1 = (1, 2, 3)
tuple2 = tuple1
print(tuple2 is tuple1)     # Check if both variables point to the same object
tuple2 += (4, 5)
print(tuple2 is tuple1)     # After modification, they are different objects
# Output:
# False
# True

# Ordered (characters maintain their order)
tuple3 = (1, 2, 3)
print(list(tuple3))
# Output: [1, 2, 3]

# Indexed (characters can be accessed by index)
tuple4 = (3, 2, 1)
print(tuple4[0])
# Output: 3

# Heterogeneous (can contain different types)
tuple5 = (1, "two", 3.0, True)
print(tuple5)
# Output: (1, 'two', 3.0, True)

Set Data

# Set data
# ------------------------------------------------------------------------------
# The set type in Python is a collection of elements with unique values. It
# used to remove duplicates from a collection and to perform set operations like
# union, intersection, and difference. The set type has the following
# characteristics:
#
# - Passed by reference: Changes to one variable affect all references
# - Unique: A set cannot contain duplicate elements.
# - Mutable: You can add or remove elements from a set.
# - Unordered: The elements in a set do not have a specific order.
# - Heterogeneous: A set can contain elements of different data types.
# - Unindexed: You cannot access elements in a set by index

# Passing by reference
s1 = {1, 2, 3}
s2 = s1
s2.add(4)
print(s1, s2)
# Output: {1, 2, 3, 4} {1, 2, 3, 4}

# All elements are unique
a = {1, 1, 2, 2, 3, 3}
print(id(a), a, sep=": ")
# Output: {1, 2, 3}

# Mutable: You can add and remove elements
a.add(4)
print(id(a), a, sep=": ")
# Output: {1, 2, 3, 4}

# Unordered: The order of elements is not guaranteed
b = {3, 1, 2}
print(id(b), b, sep=": ")
# Output: {1, 2, 3}

# Heterogeneous: A set can contain elements of different data types
c = {1, "two", 3.0, (4, 5)}
print(id(c), c, sep=": ")
# Output: {1, 'two', 3.0, (4, 5)}

# Unindexed: You cannot access elements by index
d = {1, 2, 3}
print(d[0])
# Output: TypeError: 'set' object is not subscriptable

Dictionary Data

# Dictionary data
# ------------------------------------------------------------------------------
# This code creates and manipulates dictionaries in Python. A dictionary is
# a collection of key–value pairs where each key is unique. If a key
# appears more than once, the last assigned value is retained. The
# dictionary variable has the following properties:
#
# - Passed by reference: Changes to one variable affect all references
# - Key access: You can access values using their keys
# - Unique keys: Each key must be unique within the dictionary
# - Mutable: Can be modified after creation
# - Ordered: The order of inserted items is maintained (as of Python 3.7)
# - Heterogeneous: Can contain keys and values of different types

# Passed by reference (changes to one variable affect all references)
d1 = {'junior': 1, 'mid': 2, 'senior': 3}
d2 = d1
d2['junior'] = 4
print(d1, d2)
# Output: {'junior': 4, 'mid': 2, 'senior': 3} {'junior': 4, 'mid': 2, 'senior': 3}

# Key access
a = {'junior': 1, 'mid': 2, 'senior': 3}
print(a['mid'])
# Output: 2

# Only unique keys (last one wins)
b = {'junior': 1, 'mid': 2, 'senior': 3, 'senior': 4}
print(b)
# Output: {'junior': 1, 'mid': 2, 'senior': 4}

# Mutable
c = {'junior':1, 'mid': 2}
c.update({'senior': 3})
print(c)
# Output: {'junior': 1, 'mid': 2, 'senior': 3}

# Ordered (as of Python 3.7)
d = {'mid': 2, 'senior': 3, 'junior': 1}
print(d)
# Output: {'mid': 2, 'junior': 1, 'senior': 3}

# Heterogeneous
e = {'junior': 1, 'mid': 2.5, 'senior': 'three'}
print(e)
# Output: {'junior': 1, 'mid': 2.5, 'senior': 'three'}

String Data

# String data
# ------------------------------------------------------------------------------
# String variables in Python can be defined using single or double quotes.
# Both types of quotes are valid and can be used interchangeably. The string
# has the following characteristics:
#
# - Immutable: Once created, the string cannot be changed.
# - Ordered: The characters in the string maintain their order.
# - Indexed: Each character in the string can be accessed by its index.
# - Homogeneous: All characters in the string are of the same type.

# Immutable (a change creates a new object)
string1 = "Hello, world!"
string2 = string1.replace("world", "Python")
print(string2 is string1)
# Output: False

# Ordered (characters maintain their order)
string3 = "Python"
print(list(string3))
# Output: ['P', 'y', 't', 'h', 'o', 'n']

# Indexed (characters can be accessed by index)
string4 = "Hello, world!"
print(string4[0])
# Output: 'H'

# Homogeneous (all characters are of the same type)
string5 = "Python" + 3
# Output: TypeError: can only concatenate str (not "int") to str

None Type

# None
# ------------------------------------------------------------------------------
# The None type in Python is used to show that the variable is not assigned
# any value. It is useful when you want to indicate that a variable is
# intentionally left empty or when a function does not return a value, e.g.
# the `print` function.

a = None
print(a)
# Output: None

# The print function returns None
print(print())
# Output: None

Mutable Variables

# Mutable variables in Python
# ------------------------------------------------------------------------------
# Some variables in Python are mutable, meaning their value can be changed after
# they are created allowing you to dynamic modification of their contents. The
# most common mutable variable types in Python are lists, dictionaries, and
# sets.
#
# In Python mutable variables are passed by reference, meaning that if you
# assign a mutable variable to another variable, both variables will refer to
# the same object in memory. This is a source of confusion for many a
# Python developers, as it can lead to unexpected behavior.

# Create a mutable variable (list)
test = [1, 2, 3, 4]
print("ID: {}".format(id(test)))
# Output:
# ID: 140123456789456

# Modify the mutable variable and prove that it is still the same object
test.append(5)
print("ID: {}".format(id(test)))
# Output:
# ID: 140123456789456

# An assignment to another variable will create a reference to the same object
reference = test
print("ID: {}".format(id(reference)))
# Output:
# ID: 140123456789456

Immutable Variables

# Immutable variables in Python
# ------------------------------------------------------------------------------
# Some variables in Python are immutable, meaning their value cannot be changed
# after they are created. This is in contrast to mutable variables, which can
# be modified after creation.
#
# For example, integer constants (also known as literals) are immutable in
# Python. When you create a constant, it has a unique identifier (id)
# that remains constant throughout the program's execution. Trying to reassign
# an immutable constant to a new value will result in a SyntaxError.

test = 1
print("Testing immutable constant 1 (int)")
print("ID of test   : {}".format(id(test)))
print("ID of 1      : {}".format(id(1)))
# Output:
# Testing immutable constant 1 (int)
# ID of test   : 140737488346112
# ID of 1      : 140737488346112

test = "A"
print("Testing immutable constant 'A' (str)")
print("ID of test   : {}".format(id(test)))
print("ID of 'A'    : {}".format(id("A")))
# Output:
# Testing immutable constant 'A' (str)
# ID of test   : 140737488346112
# ID of 'A'    : 140737488346112

# Trying to reassign an immutable constant will raise a SyntaxError
1 = 2
# Output:
# SyntaxError: cannot assign to literal ...

Get Variable Type

# Get the type of a variable
# ------------------------------------------------------------------------------
# Python offers a way to inspect the type of a variable using `type()`. Type
# is a special class in Python that servers many purposes, including
# determining the type of a variable. When invoked on a variable, it returns
# the type of that variable, which can be useful for troubleshooting or understanding
# the data being worked with.

var = 100
print(var, type(var))
# Output: 100 <class 'int'>

var = 100.0
print(var, type(var))
# Output: 100.0 <class 'float'>