Python Fundamentals for GATE CS – Lists, OOP, Comprehensions | EngineeringHulk


Python Fundamentals for GATE CS

Data types, built-in structures, list comprehensions, functions, OOP in Python — everything GATE CS tests in Python, with output-prediction examples.

Last updated: April 2026  |  GATE CS 2024–2026 syllabus

Key Takeaways for GATE

  • Python is dynamically typed and garbage collected. Everything is an object.
  • Mutable: list, dict, set, bytearray. Immutable: int, float, str, tuple, frozenset.
  • List comprehension: [expr for x in iter if cond] — O(n), creates a new list.
  • Python passes arguments by object reference — rebinding inside function doesn’t affect caller; mutating the object does.
  • is checks identity (same object in memory); == checks equality (same value).
  • OOP: self is explicit first parameter. __init__ is constructor. __str__ for string representation.
  • Generator uses yield — lazy evaluation, O(1) memory vs O(n) for list.

1. Data Types & Mutability

TypeMutableOrderedDuplicatesSyntax
int, float, complexNox = 42
strNoYesYess = "hello"
listYesYesYes[1, 2, 3]
tupleNoYesYes(1, 2, 3)
dictYesYes (3.7+)Keys: No{'a': 1}
setYesNoNo{1, 2, 3}
frozensetNoNoNofrozenset({1,2})
Identity vs equality:

a = [1, 2, 3]
b = [1, 2, 3]
a == b   # True  (same values)
a is b   # False (different objects)

c = a
a is c   # True  (same object)

Small int caching: Python caches integers -5 to 256. a = 100; b = 100; a is b → True. a = 1000; b = 1000; a is b → False (implementation-defined).

2. Built-in Data Structures

List — O(1) append/access, O(n) insert/delete at arbitrary position:

a = [1, 2, 3]
a.append(4)     # O(1) amortised
a.insert(0, 0)  # O(n) — shifts elements
a.pop()         # O(1) — removes last
a.pop(0)        # O(n) — removes first, shifts

Dict — O(1) average get/set/delete (hash table):

d = {'a': 1, 'b': 2}
d['c'] = 3       # insert
d.get('x', 0)   # get with default — no KeyError
del d['a']
'b' in d         # O(1) membership test

Set — O(1) add/remove/membership, O(n) union/intersection:

s = {1, 2, 3}
s.add(4)
s.discard(10)    # no error if not present (unlike remove())
s1 & s2          # intersection
s1 | s2          # union
s1 - s2          # difference

3. Comprehensions

List comprehension:

squares = [x**2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
flat = [x for row in matrix for x in row]  # nested

Dict comprehension:

sq_dict = {x: x**2 for x in range(5)}  # {0:0, 1:1, 2:4, 3:9, 4:16}

Set comprehension:

unique_sq = {x**2 for x in [-2,-1,0,1,2]}  # {0, 1, 4}

Generator expression (lazy):

gen = (x**2 for x in range(10))  # O(1) memory, computed on demand
sum(gen)   # 285

4. Functions

Default arguments (evaluated once at definition):

def f(x, lst=[]):   # TRAP: lst shared across all calls!
    lst.append(x)
    return lst
f(1)  # [1]
f(2)  # [1, 2]  — NOT [2]!

# Fix: def f(x, lst=None): if lst is None: lst = []

*args and **kwargs:

def f(*args, **kwargs):
    print(args)    # tuple of positional args
    print(kwargs)  # dict of keyword args

Lambda (anonymous function):

sq = lambda x: x**2
sorted_list = sorted(data, key=lambda x: x[1])

map, filter, reduce:

list(map(lambda x: x*2, [1,2,3]))     # [2, 4, 6]
list(filter(lambda x: x%2==0, range(10)))  # [0, 2, 4, 6, 8]
from functools import reduce
reduce(lambda a,b: a+b, [1,2,3,4])   # 10

5. Scope & Closures (LEGB Rule)

Python looks up names in order: Local → Enclosing → Global → Built-in.

global keyword:

x = 10
def f():
    global x    # without this, x = 20 creates a new local x
    x = 20
f(); print(x)   # 20

nonlocal keyword (for closures):

def outer():
    count = 0
    def inner():
        nonlocal count
        count += 1
        return count
    return inner
c = outer()
c()  # 1
c()  # 2 — count persists in enclosing scope

6. OOP in Python

class Animal:
    species = "Unknown"          # class variable (shared)

    def __init__(self, name):
        self.name = name         # instance variable

    def speak(self):             # instance method
        return f"{self.name} speaks"

    @classmethod
    def create(cls, name):       # class method — gets cls, not self
        return cls(name)

    @staticmethod
    def info():                  # no self or cls
        return "I am an animal"

class Dog(Animal):
    def speak(self):             # override
        return f"{self.name} barks"

d = Dog("Rex")
print(d.speak())                 # "Rex barks" (polymorphism)
print(isinstance(d, Animal))    # True

Special methods (dunder):
__init__ (constructor), __str__ (str()), __repr__ (repr()), __len__ (len()), __eq__ (==), __lt__ (<)

7. Generators & Iterators

Generator function:

def count_up(n):
    i = 0
    while i < n:
        yield i      # pauses here, returns i, resumes on next()
        i += 1

g = count_up(3)
next(g)   # 0
next(g)   # 1
next(g)   # 2
next(g)   # StopIteration

Iterator protocol: Object with __iter__ and __next__ methods.
Generator vs list: Generator is lazy (O(1) memory at any time). List materialises all values (O(n) memory).

yield from: Delegates to a sub-generator.

def chain(*iters):
    for it in iters:
        yield from it

8. Exceptions

try:
    x = int("abc")
except ValueError as e:
    print(f"Error: {e}")
except (TypeError, KeyError):    # multiple exceptions
    pass
else:
    print("No exception")        # runs if no exception
finally:
    print("Always runs")         # runs regardless

raise ValueError("bad input")   # raise exception

Common built-in exceptions: ValueError, TypeError, KeyError, IndexError, AttributeError, ZeroDivisionError, NameError, RecursionError, StopIteration.

9. GATE Examples

GATE 2023 — What is the output?

def f(x, lst=[]):
    lst.append(x)
    return lst
print(f(1))
print(f(2))
print(f(3, []))

f(1): lst=[1] (default). f(2): same lst=[1,2] (default persists!). f(3, []): new list=[3].
Output: [1]   [1, 2]   [3]

GATE 2024 — What is the output?

a = [1, 2, 3]
b = a          # b and a refer to same list
b.append(4)
print(a)

b = a makes b point to the same list object. Mutating through b changes a.
Output: [1, 2, 3, 4]

GATE 2024 — List comprehension output:

result = [x*y for x in range(1,4) for y in range(1,4) if x != y]
print(len(result))

Pairs (x,y) where x,y in {1,2,3} and x≠y: (1,2),(1,3),(2,1),(2,3),(3,1),(3,2) = 6 pairs.
Output: 6

10. Common Mistakes

  • Mutable default argument: def f(lst=[]) — the list is created once at function definition and shared across calls. Use None as default and create a new list inside the function.
  • is vs ==: is checks object identity (same memory address), == checks value equality. For small ints and interned strings, is may give True coincidentally — don’t rely on it for equality testing.
  • Modifying a list while iterating: Skips elements or causes errors. Iterate over a copy: for x in lst[:]: or collect indices to remove and delete after.
  • Generator exhaustion: A generator can only be iterated once. After exhaustion, it’s empty. Must create a new generator or use a list.
  • Class vs instance variable: self.x = val creates instance variable. ClassName.x is class variable (shared). Assigning via self creates a new instance variable that shadows the class variable.

11. FAQ

What is the difference between a list and a tuple in Python?
Lists are mutable — you can add, remove, or change elements after creation. Tuples are immutable — contents fixed at creation. Both are ordered sequences allowing duplicates and mixed types. Tuples are faster, use less memory, and are hashable (can be dict keys or set elements). Use tuples for fixed collections and lists for collections that need to change.
How does Python manage memory?
Python uses automatic garbage collection primarily via reference counting — each object tracks how many references point to it; when the count reaches 0 the object is freed. The gc module handles cyclic references (A → B → A with no other references). Python also caches small integers (-5 to 256) and interned strings to avoid repeated allocation.
What is a generator in Python?
A generator is a function containing yield that returns values lazily — producing one value at a time only when requested via next(). It maintains state between calls and uses O(1) memory regardless of how many values it can produce. Generators are ideal for large datasets, infinite sequences, and pipelines where you don’t need all values simultaneously.
What is the difference between @staticmethod and @classmethod in Python?
@staticmethod: no implicit first argument — it’s a regular function grouped in the class namespace. Cannot access class or instance state without explicit reference. @classmethod: takes cls as first argument (the class itself) — can access and modify class-level state, call other class methods, and is used for alternative constructors (factory methods).