Cheatsheet: Python Basic Syntax¶

1. Variables and Data Types¶

  • Assigning Variables: Use the = sign to assign values to variables.
    x = 5          # Integer
    name = "Alice" # String
    pi = 3.14      # Float
    is_valid = True # Boolean
    
  • Common Data Types:
    • Integer: Whole numbers (e.g., 10, -5)
    • Float: Decimal numbers (e.g., 3.14, -2.5)
    • String: Text enclosed in quotes (e.g., "Hello", 'Python')
    • Boolean: True or False

2. Comments¶

  • Single-line Comment: Use # to add a comment.
    # This is a single-line comment
    
  • Multi-line Comment: Use triple quotes """ ... """ or ''' ... '''.
    """
    This is a multi-line comment
    Useful for documentation
    """
    

3. Printing to the Console¶

  • Print Statement:
    print("Hello, world!") # Outputs: Hello, world!
    print(x)               # Outputs the value of x
    

4. Basic Arithmetic Operators¶

Operator Description Example
+ Addition 3 + 2 → 5
- Subtraction 3 - 2 → 1
* Multiplication 3 * 2 → 6
/ Division 3 / 2 → 1.5
// Floor Division 3 // 2 → 1
% Modulus (remainder) 3 % 2 → 1
** Exponentiation 3 ** 2 → 9

5. String Operations¶

  • Concatenation: Use + to combine strings.
    full_name = "John" + " " + "Doe" # Outputs: John Doe
    
  • Repetition: Use * to repeat strings.
    greeting = "Hi! " * 3 # Outputs: Hi! Hi! Hi!
    
  • String Length: Use len() to get the length of a string.
    length = len("Python") # Outputs: 6
    
  • Accessing Characters: Use indexing (starts at 0).
    word = "Python"
    print(word[0])  # Outputs: 'P'
    print(word[-1]) # Outputs: 'n' (last character)
    

6. Lists (Arrays)¶

  • Creating a List:
    numbers = [1, 2, 3, 4, 5]
    names = ["Alice", "Bob", "Charlie"]
    
  • Accessing Elements: Use indexing.
    print(numbers[0])  # Outputs: 1
    print(names[2])    # Outputs: Charlie
    
  • Modifying Lists:
    numbers[0] = 10 # Changes first element to 10
    
  • Common List Operations:
    numbers.append(6)       # Adds 6 to the end
    numbers.pop()           # Removes and returns the last element
    numbers.remove(10)      # Removes the first occurrence of 10
    length = len(numbers)   # Returns the length of the list
    

7. Conditional Statements¶

  • If, Elif, Else:
    age = 18
    if age >= 18:
        print("Adult")
    elif age > 12:
        print("Teenager")
    else:
        print("Child")
    
  • Comparison Operators:
Operator Description
== Equal to
!= Not equal to
< Less than
> Greater than
<= Less than or equal to
>= Greater than or equal

8. Loops¶

  • For Loop: Iterate over a sequence.
    for i in range(5):  # Outputs: 0, 1, 2, 3, 4
        print(i)
    
  • While Loop: Repeat as long as a condition is true.
    count = 0
    while count < 5:
        print(count)
        count += 1
    

9. Functions¶

  • Defining and Calling Functions:

    def greet(name):
        return "Hello, " + name
    
    message = greet("Alice") # Outputs: Hello, Alice
    print(message)
    
  • Function with Default Parameters:

    def greet(name="Guest"):
        print("Hello, " + name)
    
    greet()          # Outputs: Hello, Guest
    greet("Alice")   # Outputs: Hello, Alice
    

10. Common Mistakes to Avoid¶

  • Indentation: Python relies on indentation (usually 4 spaces) to define code blocks.
    if x > 0:
        print("Positive") # Correct
    print("This line is outside the if block") # Correct
    
  • Using = for comparison: Use == for equality comparison, = is for assignment.
    if x == 5:  # Correct
        print("x is 5")
    
  • Mismatched Quotes: Make sure strings use matching pairs of quotes (' ' or " ").
    name = "Alice"  # Correct
    

Python Lists: A Detailed Guide¶

1. What is a List?¶

  • A list in Python is an ordered collection of items (elements) that can hold different data types like integers, strings, or even other lists.
  • Lists are mutable, meaning their elements can be changed after creation.

2. Creating a List¶

  • Empty List:
    my_list = []
    
  • List with Elements:
    numbers = [1, 2, 3, 4]
    fruits = ["apple", "banana", "cherry"]
    mixed = [1, "apple", 3.5, True]  # Lists can hold different data types
    

3. Accessing Elements in a List¶

  • Indexing: Use square brackets [] to access elements by their position (index) in the list. Indexing starts at 0.
    fruits = ["apple", "banana", "cherry"]
    print(fruits[0])  # Output: "apple"
    print(fruits[2])  # Output: "cherry"
    
  • Negative Indexing: Use negative numbers to access elements from the end of the list.
    print(fruits[-1])  # Output: "cherry"
    print(fruits[-2])  # Output: "banana"
    

4. Slicing Lists¶

  • Slicing: Extract a part (slice) of the list by specifying the start and end indices.
    numbers = [1, 2, 3, 4, 5]
    print(numbers[1:4])  # Output: [2, 3, 4]
    
    • Note: The slice numbers[1:4] includes elements at indices 1 to 3, but not 4.
  • Slicing with Step: Add a step to skip elements.
    print(numbers[::2])  # Output: [1, 3, 5] (every second element)
    

5. Basic List Operations¶

  • Length: Use len() to find the number of elements in the list.
    print(len(fruits))  # Output: 3
    
  • Concatenation: Combine two lists using the + operator.
    list1 = [1, 2, 3]
    list2 = [4, 5, 6]
    combined = list1 + list2
    print(combined)  # Output: [1, 2, 3, 4, 5, 6]
    
  • Repetition: Use * to repeat the elements in a list.
    repeated_list = [1, 2] * 3
    print(repeated_list)  # Output: [1, 2, 1, 2, 1, 2]
    
  • Membership Check: Use in to check if an element is in the list.
    print("apple" in fruits)  # Output: True
    

6. Modifying Lists¶

  • Appending Elements: Use .append() to add an element to the end of the list.
    fruits.append("orange")
    print(fruits)  # Output: ["apple", "banana", "cherry", "orange"]
    
  • Inserting Elements: Use .insert(index, element) to add an element at a specific position.
    fruits.insert(1, "blueberry")
    print(fruits)  # Output: ["apple", "blueberry", "banana", "cherry"]
    
  • Extending a List: Use .extend() to add multiple elements from another list.
    fruits.extend(["kiwi", "mango"])
    print(fruits)  # Output: ["apple", "banana", "cherry", "kiwi", "mango"]
    
  • Updating Elements: Assign a new value to an element using its index.
    fruits[0] = "grape"
    print(fruits)  # Output: ["grape", "banana", "cherry"]
    

7. Removing Elements¶

  • Remove by Value: Use .remove(value) to delete the first occurrence of a value.
    fruits.remove("banana")
    print(fruits)  # Output: ["apple", "cherry"]
    
  • Remove by Index: Use .pop(index) to remove and return the element at the specified index. If no index is given, it removes the last element.
    fruits.pop(1)
    print(fruits)  # Output: ["apple"] (removes "cherry")
    
  • Clear the List: Use .clear() to remove all elements in the list.
    fruits.clear()
    print(fruits)  # Output: []
    

8. Other Useful List Functions¶

  • Finding the Length: len(my_list) returns the number of elements.
  • Counting Occurrences: .count(value) returns how many times a value appears in the list.
    numbers = [1, 2, 2, 3, 4, 2]
    print(numbers.count(2))  # Output: 3
    
  • Finding Index: .index(value) returns the first index of the value.
    print(fruits.index("cherry"))  # Output: 1
    
  • Sorting: .sort() sorts the list in ascending order.
    numbers.sort()
    print(numbers)  # Output: [1, 2, 3, 4]
    
  • Reversing: .reverse() reverses the order of the list.
    numbers.reverse()
    print(numbers)  # Output: [4, 3, 2, 1]
    

9. Looping Through Lists¶

  • For Loop: Use a for loop to iterate over each element in the list.
    for fruit in fruits:
        print(fruit)
    
    • Output:
      apple
      banana
      cherry
  • Using enumerate: Get both the index and the element.
    for index, fruit in enumerate(fruits):
        print(index, fruit)
    
    • Output:
      0 apple
      1 banana
      2 cherry

10. Copying Lists¶

  • Shallow Copy: Create a copy of the list using .copy() or slicing.
    new_list = fruits.copy()
    another_list = fruits[:]  # Both work the same way
    

11. List Comprehensions (Optional)¶

  • A concise way to create lists. For example, creating a list of squares:
    squares = [x ** 2 for x in range(5)]
    print(squares)  # Output: [0, 1, 4, 9, 16]
    

Common Mistakes to Avoid¶

  • Index Out of Range: Trying to access an index that doesn't exist (e.g., my_list[10] if the list has fewer than 11 elements).
  • Using = for copying: new_list = old_list copies the reference, not the actual list, so changes to new_list affect old_list. Use .copy() or slicing for an actual copy.
  • Mixing Data Types: Be cautious when using lists with mixed data types, especially when performing numerical operations.

Nested Lists in Python¶

1. What are Nested Lists?¶

  • A nested list is a list that contains other lists as its elements. This structure allows you to create multi-dimensional data structures like vectors, matrices, and tensors.
  • Example of a Nested List:
    nested_list = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    
    Here, nested_list is a 2-dimensional list (matrix) containing three sublists.

2. Using Nested Lists for Mathematical Structures¶

2.1. Vectors (1D Lists)¶
  • In mathematics, a vector is a 1-dimensional array of numbers. In Python, you can represent a vector using a simple list.
  • Example:
    vector = [1, 2, 3]
    
  • Accessing Elements: You can access elements of a vector using a single index.
    print(vector[0])  # Output: 1
    print(vector[2])  # Output: 3
    
2.2. Matrices (2D Lists)¶
  • A matrix is a 2-dimensional array of numbers, often represented as a grid. In Python, you can represent a matrix using a list of lists (nested lists).
  • Example:
    matrix = [
        [1, 2, 3],
        [4, 5, 6],
        [7, 8, 9]
    ]
    
    • Each sublist represents a row in the matrix.
  • Accessing Elements: Use two indices – the first for the row and the second for the column.
    print(matrix[0][1])  # Output: 2 (1st row, 2nd column)
    print(matrix[2][2])  # Output: 9 (3rd row, 3rd column)
    
  • Modifying Elements: You can modify an element by specifying its row and column indices.
    matrix[1][1] = 10  # Changes the element in the 2nd row, 2nd column to 10
    print(matrix)  # Output: [[1, 2, 3], [4, 10, 6], [7, 8, 9]]
    
2.3. Tensors (3D Lists and Higher)¶
  • A tensor is a multi-dimensional array that can extend beyond 2 dimensions. In Python, you can represent a tensor using nested lists with more levels.
  • Example of a 3D Tensor:
    tensor = [
        [
            [1, 2], 
            [3, 4]
        ],
        [
            [5, 6], 
            [7, 8]
        ],
        [
            [9, 10], 
            [11, 12]
        ]
    ]
    
    • This example is a 3-dimensional tensor with shape (3, 2, 2), which means it contains 3 blocks (or matrices), each with 2 rows and 2 columns.
  • Accessing Elements: Use three indices to access elements in a 3D tensor.
    print(tensor[0][1][1])  # Output: 4 (1st block, 2nd row, 2nd column)
    print(tensor[2][0][1])  # Output: 10 (3rd block, 1st row, 2nd column)
    
  • Modifying Elements: You can change elements in the same way.
    tensor[1][0][0] = 100
    print(tensor)  # Output: [[[1, 2], [3, 4]], [[100, 6], [7, 8]], [[9, 10], [11, 12]]]
    

3. Working with Nested Lists¶

  • Looping through Nested Lists:

    • For matrices (2D lists), you can use nested for loops to iterate over rows and elements within each row.
      for row in matrix:
          for element in row:
              print(element, end=' ')
      # Output: 1 2 3 4 10 6 7 8 9
      
    • For tensors (3D lists), use three nested loops to iterate over each dimension.
      for block in tensor:
          for row in block:
              for element in row:
                  print(element, end=' ')
      # Output: 1 2 3 4 100 6 7 8 9 10 11 12
      
  • List Comprehensions with Nested Lists:

    • You can use list comprehensions to create nested lists or perform operations on them.
    • Example: Creating a 3x3 matrix initialized to zero.
      matrix = [[0 for _ in range(3)] for _ in range(3)]
      print(matrix)  # Output: [[0, 0, 0], [0, 0, 0], [0, 0, 0]]
      
    • Example: Transposing a matrix (swapping rows and columns).
      matrix = [
          [1, 2, 3],
          [4, 5, 6],
          [7, 8, 9]
      ]
      transpose = [[matrix[j][i] for j in range(len(matrix))] for i in range(len(matrix[0]))]
      print(transpose)  # Output: [[1, 4, 7], [2, 5, 8], [3, 6, 9]]
      

4. Common Mistakes to Avoid¶

  • Index Out of Range: Make sure that the indices you use to access elements are within the range of the nested list's dimensions.
    print(matrix[3][0])  # Error! IndexError: list index out of range
    
  • Irregular Nested Lists: While Python allows nested lists with varying lengths (e.g., a "jagged" list), it can lead to unexpected errors when performing operations assuming uniform structure.
    jagged_list = [[1, 2], [3, 4, 5], [6]]
    # Accessing jagged_list[1][2] is valid, but jagged_list[2][1] will raise an IndexError.
    

5. Practical Applications of Nested Lists¶

  • Vectors: Used in simple mathematical calculations like finding the sum of elements or dot products.
  • Matrices: Used in linear algebra, image processing, and representing tables or grids.
  • Tensors: Used in advanced applications like deep learning, where data often has multiple dimensions (e.g., images are often represented as 3D tensors).

By understanding and effectively using nested lists, you can handle a variety of data structures and mathematical computations in Python, making it a valuable skill for both beginner and advanced programming tasks.

Mathematical Operations in Python¶

1. Basic Arithmetic Operations¶

Operation Mathematical Notation Python Code
Addition $a + b$ a + b
Subtraction $a - b$ a - b
Multiplication $a \times b$ a * b
Division $\frac{a}{b}$ a / b
Floor Division $\lfloor \frac{a}{b} \rfloor$ a // b
Modulus (Remainder) $a \mod b$ a % b
Exponentiation $a^b$ a ** b

2. Summation of a List¶

To calculate the sum of elements in a list:

  • Mathematical Notation: $\sum_{i=1}^{n} x_i$
  • Python Code:
    numbers = [1, 2, 3, 4, 5]
    total_sum = 0
    for num in numbers:
        total_sum += num  # Output: 15
    

3. Finding the Average¶

  • Mathematical Notation: $\text{average} = \frac{1}{n} \sum_{i=1}^{n} x_i$
  • Python Code:
    numbers = [1, 2, 3, 4, 5]
    total_sum = 0
    for num in numbers:
        total_sum += num
    average = total_sum / len(numbers)  # Output: 3.0
    

4. Vector Operations¶

Vector Addition¶

  • Mathematical Notation: If $\mathbf{a} = [a_1, a_2, \dots, a_n]$ and $\mathbf{b} = [b_1, b_2, \dots, b_n]$, then: $$ \mathbf{a} + \mathbf{b} = [a_1 + b_1, a_2 + b_2, \dots, a_n + b_n] $$
  • Python Code:
    a = [1, 2, 3]
    b = [4, 5, 6]
    c = []
    for i in range(len(a)):
        c.append(a[i] + b[i])  # Output: [5, 7, 9]
    

Scalar Multiplication¶

  • Mathematical Notation: If $\mathbf{a} = [a_1, a_2, \dots, a_n]$ and scalar $c$, then: $$ c \mathbf{a} = [c \times a_1, c \times a_2, \dots, c \times a_n] $$
  • Python Code:
    a = [1, 2, 3]
    scalar = 3
    result = []
    for x in a:
        result.append(scalar * x)  # Output: [3, 6, 9]
    

Dot Product¶

  • Mathematical Notation: If $\mathbf{a} = [a_1, a_2, \dots, a_n]$ and $\mathbf{b} = [b_1, b_2, \dots, b_n]$, then: $$ \mathbf{a} \cdot \mathbf{b} = \sum_{i=1}^{n} a_i b_i $$
  • Python Code:
    a = [1, 2, 3]
    b = [4, 5, 6]
    dot_product = 0
    for i in range(len(a)):
        dot_product += a[i] * b[i]  # Output: 32
    

5. List (Vector) Sum¶

  • Mathematical Notation: $\sum_{i=1}^{n} a_i$
  • Python Code:
    vector = [1, 2, 3, 4, 5]
    vector_sum = 0
    for num in vector:
        vector_sum += num  # Output: 15
    

6. Norm of a Vector¶

  • Mathematical Notation (Euclidean Norm): For a vector $\mathbf{a} = [a_1, a_2, \dots, a_n]$, $$ \|\mathbf{a}\| = \sqrt{\sum_{i=1}^{n} a_i^2} $$
  • Python Code:

    import math
    
    a = [3, 4]
    norm = 0
    for x in a:
        norm += x ** 2
    norm = math.sqrt(norm)  # Output: 5.0
    

7. Matrix Operations¶

Matrix Addition¶

  • Mathematical Notation: If $A = [a_{ij}]$ and $B = [b_{ij}]$ are matrices of the same dimension, then: $$ C = A + B = [a_{ij} + b_{ij}] $$
  • Python Code:

    A = [[1, 2], [3, 4]]
    B = [[5, 6], [7, 8]]
    C = []
    
    for i in range(len(A)):
        row = []
        for j in range(len(A[0])):
            row.append(A[i][j] + B[i][j])
        C.append(row)
    # Output: [[6, 8], [10, 12]]
    

Matrix-Vector Multiplication¶

  • Mathematical Notation: If $A$ is an $m \times n$ matrix and $\mathbf{v}$ is a vector of size $n$: $$ \mathbf{b} = A \mathbf{v} = \left[ \sum_{j=1}^{n} a_{ij} v_j \right] $$
  • Python Code:

    A = [[1, 2], [3, 4], [5, 6]]
    v = [7, 8]
    result = []
    
    for i in range(len(A)):
        row_sum = 0
        for j in range(len(v)):
            row_sum += A[i][j] * v[j]
        result.append(row_sum)
    # Output: [23, 53, 83]
    

Transpose of a Matrix¶

  • Mathematical Notation: If $A$ is an $m \times n$ matrix, its transpose $A^T$ is an $n \times m$ matrix. $$ A^T = [a_{ji}] $$
  • Python Code:

    A = [[1, 2, 3], [4, 5, 6]]
    transpose = []
    
    for i in range(len(A[0])):  # Number of columns in original matrix
        new_row = []
        for j in range(len(A)):  # Number of rows in original matrix
            new_row.append(A[j][i])
        transpose.append(new_row)
    # Output: [[1, 4], [2, 5], [3, 6]]
    

Classes in Python¶

1. What is a Class?¶

  • A class is a blueprint or template for creating objects (instances). Think of it as a way to define a new "type" of object.
  • It allows you to bundle data (called attributes) and functions (called methods) that operate on the data into a single unit.
  • Using classes helps to organize code, model real-world entities, and implement the principles of object-oriented programming (OOP), like encapsulation, inheritance, and polymorphism.

2. What is an Object?¶

  • An object is an instance of a class. When you create an object, you are using the class as a blueprint to make a specific version of that object with its own data.
  • For example, if you have a class Dog, you can create objects like dog1 and dog2, each representing a different dog with its own unique attributes (e.g., name, age).

3. Defining a Class in Python¶

  • To define a class, use the class keyword, followed by the class name. Class names are usually written in PascalCase (each word capitalized).
  • Inside the class, you define methods and attributes that belong to it.

Example: A Simple Dog Class¶

class Dog:
    # Constructor method: Initializes an object of the class
    def __init__(self, name, age):
        self.name = name  # Attribute to store the dog's name
        self.age = age    # Attribute to store the dog's age

    # Method to make the dog bark
    def bark(self):
        print(f"{self.name} says: Woof!")

# Creating an object (instance) of the Dog class
my_dog = Dog("Buddy", 3)

4. Components of a Class¶

4.1. The __init__ Method (Constructor)¶

  • __init__ is a special method (also known as a constructor) that is automatically called when you create a new object.
  • It initializes the object's attributes with values provided when creating the object.
  • self is a reference to the current instance of the class and is used to access attributes and methods within the class.

Example: Using __init__ to Initialize Attributes¶

class Car:
    def __init__(self, make, model, year):
        self.make = make    # Set the make of the car
        self.model = model  # Set the model of the car
        self.year = year    # Set the year of the car

# Creating an object of the Car class
my_car = Car("Toyota", "Corolla", 2020)
print(my_car.make)  # Output: Toyota

4.2. Attributes¶

  • Attributes are variables that hold data related to the object. They are defined using self within the __init__ method or other methods.
  • In the Dog class example, name and age are attributes of the class.

4.3. Methods¶

  • Methods are functions defined inside a class that operate on objects. All methods in a class take self as their first parameter, which refers to the current instance of the class.
  • In the Dog class example, bark is a method.

4.4. The self Keyword¶

  • self refers to the instance of the class itself. It allows you to access the class's attributes and methods from within the class.
  • You must include self as the first parameter in all method definitions.

5. Creating and Using Objects (Instances)¶

  • After defining a class, you can create objects (also called instances) by calling the class as if it were a function.

Example: Creating and Using Objects¶

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def bark(self):
        print(f"{self.name} says: Woof!")

# Create objects of the Dog class
dog1 = Dog("Buddy", 3)
dog2 = Dog("Max", 5)

# Access attributes
print(dog1.name)  # Output: Buddy
print(dog2.age)   # Output: 5

# Call methods
dog1.bark()  # Output: Buddy says: Woof!
dog2.bark()  # Output: Max says: Woof!

6. Modifying Attributes¶

  • You can modify the attributes of an object directly using the dot . notation.

Example: Modifying Attributes¶

dog1.age = 4  # Changing the age of dog1
print(dog1.age)  # Output: 4

7. Adding More Methods to a Class¶

  • You can add as many methods as needed to a class to define its behavior.

Example: Adding a Method to Describe the Dog¶

class Dog:
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def bark(self):
        print(f"{self.name} says: Woof!")

    def describe(self):
        print(f"{self.name} is {self.age} years old.")

# Using the new method
dog1 = Dog("Buddy", 3)
dog1.describe()  # Output: Buddy is 3 years old.

8. Why Use Classes?¶

  • Organizing Code: Classes help organize related functions and data into a single, reusable unit.
  • Reusability: You can create multiple objects (instances) from a single class, allowing you to reuse code.
  • Modeling Real-World Entities: Classes make it easier to represent real-world objects and their behaviors in code.

9. Common Terms in Object-Oriented Programming (OOP)¶

  • Instance: A specific object created from a class.
  • Instantiate: The process of creating an object from a class.
  • Method: A function defined inside a class.
  • Attribute: A variable bound to an object or class.
  • Constructor: The __init__ method that initializes an object’s attributes.

10. Putting It All Together: A Full Example¶

Here’s a more comprehensive example that models a Bank Account:

class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner  # Account owner
        self.balance = balance  # Account balance

    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited ${amount}. New balance: ${self.balance}")

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount}. New balance: ${self.balance}")
        else:
            print(f"Insufficient funds! Current balance: ${self.balance}")

# Creating an object of BankAccount
my_account = BankAccount("Alice", 100)

# Using methods of the BankAccount class
my_account.deposit(50)  # Output: Deposited $50. New balance: $150
my_account.withdraw(30)  # Output: Withdrew $30. New balance: $120
my_account.withdraw(200)  # Output: Insufficient funds! Current balance: $120

11. Summary¶

  • A class is like a blueprint for creating objects with attributes and methods.
  • An object is an instance of a class with its own data.
  • Use __init__ to initialize an object's attributes.
  • Use methods to define the behavior of the objects.
  • self is essential to access attributes and methods within the class.
In [2]:
class BankAccount:
    def __init__(self, owner, balance=0):
        self.owner = owner  # Account owner
        self.balance = balance  # Account balance

    def deposit(self, amount):
        self.balance += amount
        print(f"Deposited ${amount}. New balance: ${self.balance}")

    def withdraw(self, amount):
        if amount <= self.balance:
            self.balance -= amount
            print(f"Withdrew ${amount}. New balance: ${self.balance}")
        else:
            print(f"Insufficient funds! Current balance: ${self.balance}")
            
# Creating an object of BankAccount
my_account = BankAccount("Alice", 100)

# Using methods of the BankAccount class
my_account.deposit(50)  # Output: Deposited $50. New balance: $150
my_account.withdraw(30)  # Output: Withdrew $30. New balance: $120
my_account.withdraw(200)  # Output: Insufficient funds! Current balance: $120
Deposited $50. New balance: $150
Withdrew $30. New balance: $120
Insufficient funds! Current balance: $120