Q1.

What is Python and what are its key features?

Python is a high-level, interpreted, general-purpose programming language widely used for web development, data analysis, artificial intelligence, scientific computing, and more. It emphasizes code readability and allows programmers to express concepts in fewer lines of code.

What is Python?

Python, created by Guido van Rossum and first released in 1991, is known for its simplicity and ease of use, making it an excellent language for beginners while also being powerful enough for complex applications. It supports multiple programming paradigms, including object-oriented, imperative, and functional programming.

Key Features of Python

  • Simple and Easy to Learn: Python has a straightforward syntax and natural language flow, making it easy to read and write.
  • Readability: Its clear and concise syntax enhances code readability and reduces the cost of program maintenance.
  • Versatile and General-Purpose: Used in a wide range of applications, including web development (Django, Flask), data science (NumPy, Pandas), machine learning (TensorFlow, scikit-learn), automation, and scripting.
  • Extensive Standard Library: Python boasts a large and comprehensive standard library that provides tools for many common programming tasks.
  • Cross-Platform Compatibility: Python code can run on various operating systems like Windows, macOS, and Linux without modification.
  • Interpreted Language: Code is executed line by line, which makes debugging easier.
  • Object-Oriented Programming (OOP): Python supports OOP concepts, allowing for modular and reusable code.
  • Dynamically Typed: Variables do not need explicit declaration of their type; the type is inferred at runtime.

Example: Hello World in Python

python
print('Hello, World!')

# This is a simple Python program.

These features collectively contribute to Python's popularity and versatility, making it a preferred choice for developers across various domains, from small scripts to large-scale enterprise applications.

Q2.

What are Python data types?

In Python, data types are classifications that specify which type of value a variable has and what type of mathematical, relational, or logical operations can be applied to it without causing an error. Python is dynamically typed, meaning you don't declare the type of a variable when you create it.

What are Data Types?

Data types represent the kind of value stored in a variable. They determine the operations that can be performed on the data and the way it is stored in memory. Python provides several built-in data types to handle various kinds of data efficiently.

Common Python Data Types

Numeric Types

These types represent numerical values. Python supports integers, floating-point numbers, and complex numbers.

  • int: Integers (e.g., 10, -500)
  • float: Floating-point numbers (e.g., 3.14, -0.001)
  • complex: Complex numbers (e.g., 1 + 2j)

Sequence Types

Sequences are ordered collections of items. They allow you to store multiple values in an organized way.

  • str: Strings (e.g., "hello", 'Python')
  • list: Ordered, mutable collection (e.g., [1, 2, 3], ['a', 'b'])
  • tuple: Ordered, immutable collection (e.g., (1, 2, 3), ('x', 'y'))

Set Types

Sets are unordered collections of unique items. They are useful for mathematical set operations.

  • set: Unordered, mutable collection of unique items (e.g., {1, 2, 3})
  • frozenset: Unordered, immutable collection of unique items

Mapping Type

Mappings store data in key-value pairs, where each key maps to a specific value.

  • dict: Unordered, mutable collection of key-value pairs (e.g., {'name': 'Alice', 'age': 30})

Boolean Type

Booleans represent truth values, often used in conditional statements.

  • bool: Represents either True or False

None Type

The None type signifies the absence of a value or a null value.

  • NoneType: Represents the None object, indicating no value

You can check the type of any variable using the type() function:

python
x = 10
y = 3.14
z = "hello"
my_list = [1, 2, 3]
my_dict = {"a": 1, "b": 2}

print(type(x))      # <class 'int'>
print(type(y))      # <class 'float'>
print(type(z))      # <class 'str'>
print(type(my_list)) # <class 'list'>
print(type(my_dict)) # <class 'dict'>
print(type(True))   # <class 'bool'>
print(type(None))   # <class 'NoneType'>

Mutability and Immutability

Python data types can also be classified as mutable or immutable. Mutable objects can be changed after they are created, while immutable objects cannot. When an immutable object is 'modified', a new object is actually created.

CategoryMutable TypesImmutable Types
Numericint, float, complex, bool
Sequenceliststr, tuple
Setsetfrozenset
Mappingdict
OtherNoneType
Q3.

Difference between list and tuple in Python?

In Python, lists and tuples are both fundamental data structures used to store collections of items. While they share some similarities, such as being ordered sequences, they have critical differences primarily related to their mutability and intended use cases.

Key Differences

The most significant distinction between lists and tuples lies in their mutability. A mutable object can be changed after it is created, while an immutable object cannot. This characteristic influences how they are used and their performance implications.

Mutability

Lists are mutable, meaning you can modify their elements, add new elements, or remove existing ones after the list has been created. Tuples, on the other hand, are immutable. Once a tuple is created, its elements cannot be changed, added, or removed. This immutability makes tuples more predictable and safer for certain operations.

Syntax

Lists are defined by enclosing their elements in square brackets [], while tuples are defined by enclosing their elements in parentheses (). A tuple with a single element requires a trailing comma.

python
my_list = [1, 2, 3]
my_tuple = (1, 2, 3)
single_element_tuple = (1,)

Operations and Methods

Due to their mutability, lists support a wider range of operations and methods for modification. Tuples have a more limited set of methods, primarily focused on querying elements.

  • List methods: append(), extend(), insert(), remove(), pop(), sort(), reverse(), etc.
  • Tuple methods: count(), index().
python
my_list = [1, 2, 3]
my_list.append(4)  # Valid
print(my_list) # Output: [1, 2, 3, 4]

my_tuple = (1, 2, 3)
# my_tuple.append(4) # This would raise an AttributeError

# Example of tuple immutability
# my_tuple[0] = 5  # This would raise a TypeError

Performance

Because tuples are immutable, Python can often optimize their creation and access, making them slightly faster than lists in some scenarios, especially when dealing with fixed collections of data. Tuples can also be used as dictionary keys because they are hashable (immutable), whereas lists cannot.

Summary Table

FeatureListTuple
MutabilityMutable (can be changed)Immutable (cannot be changed)
SyntaxSquare brackets `[]`Parentheses `()`
PerformanceSlightly slowerSlightly faster
MethodsMany (append, insert, pop, etc.)Few (count, index)
Use CaseCollections that might changeFixed collections, dictionary keys, function arguments
Q4.

What is a dictionary in Python?

A Python dictionary is an unordered collection of data values, used to store data values like a map. Unlike other data types that hold only a single value as an element, dictionaries hold key:value pairs. Dictionaries are mutable, meaning they can be changed after creation, and are optimized for retrieving values when the key is known.

What is a Dictionary?

Dictionaries are Python's implementation of a hash table type. They consist of a collection of key-value pairs. Each key maps to a specific value, allowing for efficient data retrieval. Keys must be unique and immutable (like strings, numbers, or tuples), while values can be of any data type and can be duplicated.

They are often used to store related pieces of information, such as a person's name, age, and city, where each piece of information has a descriptive label (the key).

Creating Dictionaries

Dictionaries are created by placing a comma-separated list of key:value pairs inside curly braces {}. An empty dictionary can be created with just empty curly braces.

python
# Empty dictionary
my_dict = {}
print(my_dict)
python
# Dictionary with integer keys
student = {
    101: 'Alice',
    102: 'Bob',
    103: 'Charlie'
}
print(student)

# Dictionary with mixed keys and values
person = {
    'name': 'John Doe',
    'age': 30,
    'is_student': False,
    'courses': ['Math', 'Science']
}
print(person)

Accessing Dictionary Elements

Values can be accessed using their respective keys inside square brackets [] or by using the get() method. The get() method is safer as it returns None (or a default value if specified) if the key is not found, instead of raising a KeyError.

python
person = {
    'name': 'Jane Doe',
    'age': 25,
    'city': 'New York'
}

# Accessing using square brackets
print(person['name']) # Output: Jane Doe

# Accessing using get() method
print(person.get('age'))    # Output: 25
print(person.get('country')) # Output: None
print(person.get('country', 'USA')) # Output: USA (default value)

Modifying and Deleting Elements

Dictionaries are mutable, allowing you to add new key-value pairs, update existing values, and remove elements.

python
my_settings = {
    'theme': 'dark',
    'notifications': True
}

# Adding a new key-value pair
my_settings['language'] = 'en'
print(my_settings) 

# Updating an existing value
my_settings['theme'] = 'light'
print(my_settings) 

# Deleting a key-value pair using del
del my_settings['notifications']
print(my_settings) 

# Deleting a key-value pair using pop()
# pop() also returns the value of the removed item
removed_language = my_settings.pop('language')
print(my_settings)      
print(removed_language) 

# popitem() removes and returns an arbitrary (last inserted in Python 3.7+) key-value pair
last_item = my_settings.popitem()
print(my_settings) 
print(last_item)

Common Dictionary Methods

Python dictionaries come with several built-in methods to interact with their contents.

  • keys(): Returns a new view of the dictionary's keys.
  • values(): Returns a new view of the dictionary's values.
  • items(): Returns a new view of the dictionary's key-value pairs as tuples.
  • update(other): Updates the dictionary with the key-value pairs from other, overwriting existing keys.
  • clear(): Removes all items from the dictionary.
  • copy(): Returns a shallow copy of the dictionary.

Key Characteristics

  • Unordered (pre-Python 3.7): Order of elements is not guaranteed. From Python 3.7 onwards, dictionaries maintain insertion order.
  • Mutable: Dictionaries can be changed after they are created (add, remove, or modify elements).
  • Key-Value Pairs: Data is stored as pairs where each unique key maps to a value.
  • Keys must be Unique: No two keys can be the same within a single dictionary.
  • Keys must be Immutable: Keys can be strings, numbers, or tuples (containing only immutable elements), but not lists or other mutable objects.

Understanding Key Immutability

The requirement for keys to be immutable is fundamental to how dictionaries work internally. Dictionaries use a hashing mechanism to store and retrieve data efficiently. Mutable objects can change their hash value, which would break the ability of the dictionary to find the key correctly. Therefore, mutable types like lists, sets, or other dictionaries cannot be used as keys.

Q5.

What are sets in Python?

Q6.

What is list comprehension?

List comprehension is a concise and elegant way to create lists in Python. It offers a more compact syntax than traditional for loops and often improves readability and performance.

What is List Comprehension?

List comprehension in Python provides a shorter syntax to create new lists based on existing iterables (like lists, tuples, strings, etc.). It filters and transforms elements in a single line of code, making it more 'Pythonic' than many other approaches.

Basic Syntax

python
new_list = [expression for item in iterable]

Here, expression is the operation performed on each item, and iterable is the source collection. The result of the expression for each item is collected into new_list.

Equivalent Loop

A list comprehension can often replace a multi-line for loop that appends items to a list.

python
squares = []
for i in range(1, 6):
    squares.append(i**2)
# Equivalent list comprehension:
# squares = [i**2 for i in range(1, 6)]

Why Use List Comprehension?

  • Conciseness: Reduces boilerplate code compared to traditional loops.
  • Readability: Can be easier to read and understand the intent of the code for simple transformations.
  • Performance: Often faster than a traditional for loop for creating lists, as it is optimized internally in CPython.

With Conditional Logic

You can include an if clause to filter items from the iterable.

python
even_numbers = [x for x in range(10) if x % 2 == 0]
# even_numbers will be [0, 2, 4, 6, 8]

Nested List Comprehensions

List comprehensions can be nested, similar to nested for loops, to work with multi-dimensional data structures like matrices.

python
matrix = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
flattened = [num for row in matrix for num in row]
# flattened will be [1, 2, 3, 4, 5, 6, 7, 8, 9]

Conclusion

List comprehensions are a powerful and idiomatic feature in Python for creating lists efficiently and expressively. Mastering them is key to writing clean and performant Python code for list manipulation.

Q7.

What is a lambda function?

A lambda function in Python is a small anonymous function defined using the `lambda` keyword. It can take any number of arguments but can only have one expression.

Definition

Lambda functions are also known as anonymous functions because they are not declared with the standard def keyword. They are typically used for short, single-expression functions, often as arguments to higher-order functions like map(), filter(), and sorted().

Syntax

The basic syntax for a lambda function is lambda arguments: expression. The arguments are optional, and the expression is evaluated and returned.

python
add = lambda a, b: a + b
print(add(5, 3))  # Output: 8

Key Characteristics

  • Anonymous: They don't have a name.
  • Single Expression: They can only contain one expression, which is implicitly returned.
  • Inline: Often used for short, throwaway functions that are not needed elsewhere.
  • Can take multiple arguments: Like regular functions, they can accept zero or more arguments.

Common Use Cases

  • With map(): Applying a function to all items in an iterable.
  • With filter(): Filtering elements from an iterable based on a condition.
  • With sorted(): Defining a custom sort key for sorting collections.
  • GUI event handlers: For simple callback functions.
python
numbers = [1, 2, 3, 4, 5]
squared_numbers = list(map(lambda x: x * x, numbers))
print(squared_numbers)  # Output: [1, 4, 9, 16, 25]

even_numbers = list(filter(lambda x: x % 2 == 0, numbers))
print(even_numbers)  # Output: [2, 4]

Limitations

  • Single expression only: Cannot contain multiple statements, loops, or complex logic.
  • No docstrings: Lack the ability to include a docstring for documentation.
  • Readability: Overuse or complex lambda expressions can reduce code readability compared to named functions.
Q8.

What is the difference between == and is in Python?

In Python, both `==` and `is` are used for comparisons, but they serve fundamentally different purposes. Understanding when to use each is crucial for writing correct and efficient Python code.

Understanding `==` (Equality Operator)

The == operator compares the *values* of two objects. It checks if the objects have equivalent content. This comparison is typically implemented by the __eq__ method of the objects involved.

python
list1 = [1, 2, 3]
list2 = [1, 2, 3]
list3 = [4, 5, 6]

print(f"list1 == list2: {list1 == list2}") # True (values are equal)
print(f"list1 == list3: {list1 == list3}") # False (values are different)

str1 = "hello"
str2 = "hello"
print(f"str1 == str2: {str1 == str2}") # True

Understanding `is` (Identity Operator)

The is operator compares the *identity* of two objects. It checks if two variables refer to the *exact same object in memory*. This means they must have the same memory address. It is equivalent to comparing the result of the id() function for both objects.

python
list1 = [1, 2, 3]
list2 = [1, 2, 3] # A new list object is created
list_ref = list1 # list_ref now refers to the same object as list1

print(f"list1 is list2: {list1 is list2}") # False (different objects in memory)
print(f"list1 is list_ref: {list1 is list_ref}") # True (same object in memory)

# For immutable objects like small integers and strings, Python might
# intern them, leading to 'is' returning True for value-equal objects.
a = 10
b = 10
c = 20
print(f"a is b: {a is b}") # True (often for small integers due to interning)
print(f"a is c: {a is c}") # False

str1 = "python"
str2 = "python"
str3 = "Python"
print(f"str1 is str2: {str1 is str2}") # True (often for identical strings due to interning)
print(f"str1 is str3: {str1 is str3}") # False

Key Differences

  • == compares the *values* of objects.
  • is compares the *identity* (memory address) of objects.
  • == can be overridden by the __eq__ method, defining custom equality logic.
  • is cannot be overridden; it always checks for object identity.
  • Two objects can have equal values (== is True) but be different objects in memory (is is False).
  • If two objects are the same object in memory (is is True), their values will always be equal (== is also True).

When to Use Which

Use == when you want to compare if two objects have the same content or value. This is the most common comparison operator for most data types. Use is when you specifically need to check if two variables refer to the exact same instance of an object in memory. A common use case for is is checking if a variable is None (e.g., if x is None:), because None is a singleton object.

Feature`==` (Equality Operator)`is` (Identity Operator)
PurposeCompares valuesCompares object identity (memory address)
BehaviorChecks if objects have equivalent contentChecks if variables refer to the exact same object
OverridableYes, via `__eq__` methodNo
`None` comparisonGenerally `x == None` (though `x is None` is preferred)Recommended for `None` (e.g., `x is None`)
Example Output (`[1,2]` vs `[1,2]`)True (values are equal)False (different objects)
Example Output (`a = [1,2]`, `b = a`)`a == b` is True`a is b` is True
Q9.

What is mutable and immutable in Python?

In Python, the distinction between mutable and immutable data types is fundamental to understanding how variables behave and how memory is managed. It determines whether the value of an object can be changed after it has been created.

Understanding Mutability

Mutability refers to the ability of an object to be changed after it is created. If an object is mutable, its state can be altered without creating a new object. If an object is immutable, its state cannot be changed once it's created; any operation that appears to modify it actually creates a new object.

Mutable Types

  • List
  • Dictionary
  • Set
  • Byte Array

Mutable objects can be modified in-place. This means that if you have a variable referencing a mutable object, you can change the content of that object directly without assigning a new object to the variable. All references to that object will see the updated value.

python
my_list = [1, 2, 3]
print(f"Original list: {my_list}")
my_list.append(4)
print(f"Modified list: {my_list}")

# Demonstrating shared reference
another_list = my_list
another_list.append(5)
print(f"Original list after shared modification: {my_list}")
print(f"Another list: {another_list}")

Immutable Types

  • Integer
  • Float
  • String
  • Tuple
  • Frozenset
  • Bytes

Immutable objects cannot be changed after creation. Any operation that seems to modify an immutable object, such as concatenating strings or adding to numbers, actually results in the creation of a *new* object with the updated value, and the variable is then re-assigned to this new object. The original object remains unchanged in memory (until garbage collected).

python
my_string = "Hello"
print(f"Original string: {my_string}")
print(f"ID of original string: {id(my_string)}")

my_string = my_string + " World"
print(f"Modified string (new object): {my_string}")
print(f"ID of new string: {id(my_string)}")

my_tuple = (1, 2, 3)
# my_tuple.append(4) # This would raise an AttributeError

Why Does it Matter?

Understanding mutability is crucial for several reasons: - Function Arguments: When mutable objects are passed to functions, changes made inside the function persist outside. Immutable objects passed to functions cannot be changed by the function itself (only re-assigned within the function's scope). - Dictionary Keys and Set Elements: Only immutable objects can be used as keys in dictionaries or elements in sets, because their hash value must remain constant throughout their lifetime. - Concurrency: Immutable objects are inherently thread-safe as their state cannot change, simplifying concurrent programming. - Memory Efficiency: Repeated modifications to immutable objects can lead to creation of many new objects, potentially impacting performance and memory usage if not handled carefully.

FeatureMutableImmutable
Change after creationYesNo
ID remains same after changeYesNo (new object created)
Used as dict key/set elementNoYes
ExamplesList, Dict, SetInt, Float, String, Tuple
Q10.

What are decorators in Python?