Key takeaways
- Analyst and data science interviews test practical Python more than puzzles: reading a messy file, cleaning it, grouping, joining and explaining the result.
- Know five pandas moves cold:
locandiloc,groupbywithagg,merge,pivot_table, and vectorised conditions withnp.whereorpd.cut. - pandas 3.0 made copy-on-write the only mode: chained assignment such as
df["a"][mask] = 0never changesdf, so use one.loccall.12 - In pandas 3.0, text columns get the new
strdtype by default instead ofobject, which old answers and tutorials still show.2 - AI is part of the job: 39% of data and analytics job postings in India mention AI (Indeed Hiring Lab, September 2025), so be ready to explain how you check code an AI tool wrote.14
What Python rounds test
Analyst and data science interviews in India usually test Python through the work itself: a file to clean, a question to answer with pandas, a short function to write. Algorithm puzzles come up less often than in software interviews, and how much depends on the role.
| Role | How to prepare |
|---|---|
| Data Analystpandas: filtering, grouping, merging, dates and missing values; basic Python types | Questions 1–8 and 14–24, then clean one real dataset end to end |
| Data ScientistAll of that, plus NumPy, functions and classes, and reusable code | Every question here, then a modelling project you can explain line by line |
| AI/ML EngineerClean, tested code: generators, decorators, classes, error handling, APIs | Questions 9–12 in depth, and code you’d be happy for a colleague to review |
Under each role: the Python it’s usually tested on.
Rounds take the same three shapes as SQL rounds: an online test (often “what does this code print?” plus a short problem), a live round in a notebook or shared editor, and a take-home dataset. In a live round, narrate: say what you expect each line to return before you run it. Most analyst rounds also include SQL, covered in our SQL interview questions.
The example data
The pandas answers all use this small DataFrame of an online shop’s orders, plus a table of customer segments. It has the traps interviewers like: a missing amount, repeat customers, two orders on one date and a customer with no segment. Paste it into a notebook first.
import numpy as np
import pandas as pd
orders = pd.DataFrame({
"order_id": [1001, 1002, 1003, 1004, 1005, 1006, 1007, 1008],
"customer": ["Aarav", "Diya", "Aarav", "Kabir", "Meera", "Diya", "Rohan", "Kabir"],
"city": ["Pune", "Ahmedabad", "Pune", "Delhi", "Chennai", "Ahmedabad", "Kolkata", "Delhi"],
"order_date": pd.to_datetime(["2026-09-02", "2026-09-05", "2026-09-20", "2026-10-01",
"2026-10-02", "2026-10-04", "2026-10-04", "2026-10-11"]),
"category": ["Fashion", "Fashion", "Electronics", "Home", "Fashion", "Home", "Fashion", "Home"],
"amount": [1798.0, 2499.0, 1999.0, 3398.0, 1299.0, 998.0, 2499.0, np.nan],
"status": ["delivered", "delivered", "cancelled", "delivered", "returned",
"delivered", "delivered", "delivered"],
})
segments = pd.DataFrame({
"customer": ["Aarav", "Diya", "Kabir", "Sana"],
"segment": ["Gold", "Silver", "Gold", "New"],
})
Core Python: 13 questions
Types, functions and the habits that make code readable. Online tests often show a snippet and ask what it prints, so run each of these and check you predicted the output.
1. What is the difference between mutable and immutable types?Easy
A mutable object can change in place: lists, dicts and sets. An immutable one can’t: numbers, strings, tuples and frozensets; “changing” a string makes a new one. Two names can point at the same mutable object, so a change through one shows through the other. Only immutable (hashable) values can be dict keys or set members.
city = "pune"
city.upper() # 'PUNE' is a new string; city is still 'pune'
cities = ["Pune", "Delhi"]
same = cities # a second name for the same list
same.append("Jaipur")
cities # ['Pune', 'Delhi', 'Jaipur']
point = (18.52, 73.86)
point[0] = 0 # TypeError: tuples can't be changed2. List, tuple, set or dict: when do you use which?Easy
A list is an ordered collection you’ll change. A tuple is a fixed record. A set holds unique values and answers “is it in there?” fast. A dict maps keys to values. Membership tests on sets and dicts take about the same time however big they get; on a list they get slower as it grows.
order_ids = [1001, 1002, 1002] # list: ordered, duplicates allowed
location = ("Pune", 411001) # tuple: a fixed record, can be a dict key
cities = {"Pune", "Delhi", "Pune"} # set: {'Pune', 'Delhi'}
price = {"kurta": 1299, "shoes": 2499}
"Pune" in cities # True, fast even for millions of items
price["kurta"] # 12993. What is the difference between is and ==?Easy
== asks whether two values are equal. is asks whether two names point at the very same object. Use is only for None (and True/False checks you really mean).
a = [1, 2]
b = [1, 2]
a == b # True: same value
a is b # False: two separate lists
x = None
x is None # the right way to test for None4. Shallow copy or deep copy?Medium
A shallow copy makes a new outer container but shares the objects inside it. A deep copy copies everything, all the way down. It matters as soon as you have a list of lists or a dict of lists.12
import copy
stores = [["Pune", 120], ["Delhi", 60]]
shallow = stores.copy() # new outer list, same inner lists
deep = copy.deepcopy(stores) # copies the inner lists too
shallow[0][1] = 999
stores[0][1] # 999: the inner list is shared
deep[0][1] # 1205. Why is a list as a default argument a bug?Medium
Python evaluates a default value once, when the function is defined, not on each call. A mutable default is therefore shared between calls and keeps growing. Use None and create the list inside.10
def add_item(item, basket=[]): # created once
basket.append(item)
return basket
add_item("kurta") # ['kurta']
add_item("shoes") # ['kurta', 'shoes']: the same list again
def add_item(item, basket=None): # the fix
if basket is None:
basket = []
basket.append(item)
return basket6. Write list, dict and set comprehensionsEasy
A comprehension builds a list, dict or set from a loop in one expression, with an optional filter. In round brackets it becomes a generator expression, which produces values one at a time instead of building a list.
prices = {"kurta": 1299, "shoes": 2499, "bottle": 499}
[p for p in prices.values() if p > 1000] # list: [1299, 2499]
{name: round(p * 0.9) for name, p in prices.items()} # dict: 10% off
{len(name) for name in prices} # set: {5, 6}
sum(p for p in prices.values()) # generator expression: 42977. What do *args and **kwargs do?Easy
*args collects any number of positional arguments into a tuple; **kwargs collects extra keyword arguments into a dict. The names are convention: the stars do the work. Arguments after *args can only be passed by keyword.
def total(*amounts, discount=0, **details):
print(details) # {'city': 'Pune'}
return sum(amounts) - discount
total(1299, 2499, discount=100, city="Pune") # 36988. What is a lambda, and when would you use one?Easy
A lambda is a small, nameless function written as one expression. Use it where a function is needed briefly, such as a sort key; write a normal def for anything longer or reused.
products = [("kurta", 1299), ("shoes", 2499), ("bottle", 499)]
sorted(products, key=lambda p: p[1], reverse=True)
# [('shoes', 2499), ('kurta', 1299), ('bottle', 499)]9. What is a generator, and why use one?Medium
A generator is a function that uses yield to hand back values one at a time, pausing between them. It never holds the whole sequence in memory, which is why it suits big files and streams. It can be read only once.
def read_rows(path):
with open(path) as f:
for line in f: # one line at a time
yield line.rstrip("\n").split(",")
rows = read_rows("orders.csv") # nothing is read yet
next(rows) # reads the first line only10. How do try, except, else and finally work?Medium
try runs the risky code; except handles a named error; else runs only if nothing went wrong; finally always runs, error or not. Catch specific exceptions: a bare except: hides real bugs.
def parse_amount(text):
try:
return float(text.replace("₹", "").replace(",", ""))
except (ValueError, AttributeError): # bad text, or None
return None
parse_amount("₹1,299") # 1299.0
parse_amount("N/A") # None
try:
f = open("orders.csv")
except FileNotFoundError:
print("No file")
else:
print(f.readline()) # only if open() worked
f.close()
finally:
print("Done") # always11. Explain classes, self, and class versus instance attributesMedium
A class is a blueprint for objects. __init__ sets up each new object, and self is the object itself. A class attribute is shared by every instance; an instance attribute belongs to one. A subclass inherits methods and can extend them, calling the parent with super().
class Order:
currency = "INR" # class attribute: shared
def __init__(self, order_id, amount):
self.order_id = order_id # instance attributes
self.amount = amount
def with_discount(self, pct):
return self.amount * (1 - pct / 100)
def __repr__(self):
return f"Order({self.order_id}, {self.amount})"
class UPIOrder(Order): # inherits from Order
def __init__(self, order_id, amount, upi_ref):
super().__init__(order_id, amount)
self.upi_ref = upi_ref
o = UPIOrder(1001, 1798, "UPI123")
o.with_discount(50), o.currency # (899.0, 'INR')12. What is a decorator? Write oneHard
A decorator is a function that takes a function and returns a new one that adds behaviour around it, such as timing, logging or retrying. @timed above a function is shorthand for load_orders = timed(load_orders). functools.wraps keeps the original function’s name and docstring.
import time
from functools import wraps
def timed(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
result = func(*args, **kwargs)
print(f"{func.__name__} took {time.perf_counter() - start:.3f}s")
return result
return wrapper
@timed
def load_orders(path):
return pd.read_csv(path)13. How do you handle a missing dict key?Easy
d[key] raises KeyError if the key is missing. d.get(key, default) returns a default instead. defaultdict creates the missing value for you, and Counter counts things.11
from collections import Counter, defaultdict
stock = {"kurta": 5}
stock.get("shoes", 0) # 0, no KeyError
by_city = defaultdict(list)
for city, order_id in [("Pune", 1001), ("Delhi", 1004), ("Pune", 1003)]:
by_city[city].append(order_id) # no need to check the key first
by_city["Pune"] # [1001, 1003]
Counter(["Pune", "Delhi", "Pune"]) # Counter({'Pune': 2, 'Delhi': 1})pandas: 14 questions
The heart of a data interview. Each answer runs on the orders DataFrame above.
14. What is the difference between a Series and a DataFrame?Easy
A Series is one labelled column of values. A DataFrame is a table: several Series sharing one index. Single brackets with one name give a Series; double brackets give a DataFrame.
s = orders["amount"] # Series
df = orders[["amount"]] # DataFrame with one column
type(s).__name__, type(df).__name__ # ('Series', 'DataFrame')
orders.shape # (8, 7): rows, columns15. loc or iloc?Easy
loc selects by label and includes the end of a slice. iloc selects by position and excludes the end, like Python lists. loc also takes a boolean mask, which is how you filter and pick columns in one step.3
o = orders.set_index("order_id")
o.loc[1001:1003, ["customer", "amount"]] # labels 1001 to 1003: 3 rows, end included
o.iloc[0:3, [0, 4]] # positions 0, 1, 2: end excluded
o.loc[o["amount"] > 2000, "customer"] # mask and column together16. Filter rows on several conditionsEasy
Combine conditions with & (and), | (or) and ~ (not), and wrap each one in brackets: Python’s and/or don’t work on whole columns. isin, between (both ends included) and query keep long filters readable. A NaN compares as False, so order 1008 never passes.
mask = (orders["status"] == "delivered") & (orders["amount"] > 2000)
orders[mask] # 1002, 1004, 1007
orders[orders["city"].isin(["Pune", "Delhi"])]
orders[orders["amount"].between(1000, 2000)]
orders.query("status == 'delivered' and amount > 2000")17. Count values and find duplicatesEasy
value_counts counts each value, largest first. nunique counts distinct values. duplicated marks the repeats (the first one isn’t marked), and drop_duplicates removes them. Use subset to decide which columns define a duplicate.
orders["city"].value_counts()
orders["customer"].nunique() # 5
orders.duplicated(subset=["customer"]).sum() # 3 repeat orders
orders.drop_duplicates(subset=["customer"], keep="first") # each customer's first order18. Group and aggregate: revenue by cityMedium
Named aggregation gives each output column a clear name. Note what the result hides: sum and mean skip NaN silently, so Delhi shows 2 orders but only one amount. Check for missing values before you trust a total.4
summary = (orders[orders["status"] == "delivered"]
.groupby("city", as_index=False)
.agg(orders=("order_id", "count"),
revenue=("amount", "sum"),
avg_order=("amount", "mean")))
# Delhi: orders 2, revenue 3398.0, avg_order 3398.0, because order 1008 has no amount19. agg or transform?Medium
agg returns one row per group. transform returns one value per original row, lined up with the index, so you can add a group figure as a new column, such as each order’s share of its city’s revenue. It’s the pandas version of a SQL window function.
orders["city_avg"] = orders.groupby("city")["amount"].transform("mean")
orders["share_of_city"] = (orders["amount"]
/ orders.groupby("city")["amount"].transform("sum"))20. Explain merge types, and check a merge didn’t go wrongMedium
merge joins on columns, with how set to inner, left, right, outer or cross, just like SQL joins. Two arguments catch mistakes: validate raises an error if the keys aren’t as unique as you expect, and indicator=True adds a _merge column showing where each row came from. join merges on the index; concat stacks frames.5
merged = orders.merge(segments, on="customer", how="left",
validate="many_to_one", indicator=True)
merged["_merge"].value_counts()
# both 6, left_only 2: Meera and Rohan have no segment21. pivot or pivot_table?Medium
pivot only reshapes, so it fails if any index–column pair appears twice. pivot_table aggregates the duplicates (the default is the mean) and can fill the gaps. In real data, reach for pivot_table.
orders.pivot_table(index="city", columns="category", values="amount",
aggfunc="sum", fill_value=0)
orders.pivot(index="city", columns="category", values="amount")
# ValueError: Delhi has two Home orders22. apply or a vectorised operation?Medium
apply with a Python function runs once per row, which is slow on big data. Vectorised operations work on the whole column at once in compiled code. For conditions use np.where, np.select or pd.cut; for lookups use map. Watch the NaN: pd.cut leaves it missing, while np.where quietly puts it in the “else” bucket.
# Slow: a Python function per row
orders["band"] = orders["amount"].apply(lambda x: "Large" if x >= 2500 else "Small")
# Fast: one operation on the whole column
orders["band"] = np.where(orders["amount"] >= 2500, "Large", "Small")
# Several bands, NaN stays NaN
orders["band"] = pd.cut(orders["amount"], bins=[0, 1000, 2500, np.inf],
labels=["Small", "Medium", "Large"], right=False)
# Lookups
orders["is_sale"] = orders["status"].map({"delivered": 1, "cancelled": 0, "returned": 0})23. How do you handle missing values?Medium
First count them and ask why they’re missing: a blank amount might mean a failed payment, not a zero. Then choose: drop the rows, fill them (a median, or a group’s median), or keep them and flag them. Never test for NaN with ==: NaN isn’t equal to itself.6
orders["amount"].isna().sum() # 1
orders["amount"].mean() # 2070.0, from the 7 known amounts
orders.dropna(subset=["amount"]) # drop
orders["amount"].fillna(orders["amount"].median()) # fill
orders["amount"].fillna(orders.groupby("city")["amount"].transform("median")) # fill by city
orders["amount_missing"] = orders["amount"].isna() # flag
np.nan == np.nan # False: use isna()24. Parse dates and summarise by monthMedium
Convert text to datetimes with pd.to_datetime and give the format: Indian files often write day first (05/09/2026 is 5 September), which a guess can read the wrong way. errors="coerce" turns bad dates into NaT. The .dt accessor pulls out parts, and resample groups a date index by period.7
raw = pd.Series(["05/09/2026", "20/09/2026", "not a date"])
pd.to_datetime(raw, format="%d/%m/%Y", errors="coerce") # 2026-09-05, 2026-09-20, NaT
orders["month"] = orders["order_date"].dt.to_period("M")
orders["weekday"] = orders["order_date"].dt.day_name()
monthly = orders.set_index("order_date").resample("MS")["amount"].sum()25. Running totals, moving averages and growthHard
These are the pandas versions of SQL window functions: cumsum for a running total, rolling for a moving average, shift for the previous row (SQL’s LAG) and pct_change for growth. rolling(7) counts rows; on a date index, rolling("7D") counts calendar days, so gaps don’t stretch the window.
daily = orders.groupby("order_date")["amount"].sum()
daily.cumsum() # running total
daily.rolling(7, min_periods=1).mean() # last 7 rows
daily.rolling("7D").mean() # last 7 calendar days
daily.shift(1) # previous row's value
daily.pct_change() # change on the previous row26. Why didn’t my assignment change the DataFrame?Hard
Because of chained assignment. Since pandas 3.0, copy-on-write is the only mode: every indexing step behaves like a copy, so df["col"][mask] = value updates a temporary copy, never df, and pandas warns with ChainedAssignmentError. The old SettingWithCopyWarning is gone. Do the selection and the assignment in one .loc call.12
# Never works in pandas 3.0: orders is unchanged
orders["amount"][orders["status"] == "cancelled"] = 0
# Works
orders.loc[orders["status"] == "cancelled", "amount"] = 027. A DataFrame is too big for memory. What do you do?Hard
Measure first with memory_usage(deep=True). Then load less: only the columns you need (usecols), smaller types (category for repeated text such as city names, downcast numbers), or the file in chunks that you aggregate as you go. If it still doesn’t fit, move the work into a database with SQL.8
orders.memory_usage(deep=True).sum()
orders["city"] = orders["city"].astype("category")
orders["order_id"] = pd.to_numeric(orders["order_id"], downcast="integer")
cols = ["order_id", "city", "amount"]
pd.read_csv("orders.csv", usecols=cols, dtype={"city": "category"})
totals = {}
for chunk in pd.read_csv("orders.csv", usecols=cols, chunksize=100_000):
for city, amount in chunk.groupby("city")["amount"].sum().items():
totals[city] = totals.get(city, 0) + amountNumPy: 3 questions
pandas is built on NumPy, so interviewers check that you know why it’s fast and how its shapes work.
28. Why is a NumPy array faster than a Python list?Easy
An array holds one data type in one block of memory, and its operations run as loops in compiled code. A list holds separate Python objects, and a loop over it runs in Python, one item at a time. Writing whole-array operations instead of loops is called vectorisation.
prices = np.array([1299, 2499, 1899, 499])
prices * 0.9 # one vectorised operation
[p * 0.9 for p in [1299, 2499, 1899, 499]] # a Python loop, item by item
prices.dtype # one type for every element29. What is broadcasting, and what does axis mean?Medium
axis=0 works down the rows (one result per column); axis=1 works across the columns (one result per row). Broadcasting lets arrays of different shapes combine: compare the shapes from the right, and each pair of sizes must be equal or one of them must be 1. Below, a (2, 3) array divides by a (2, 1) array.9
sales = np.array([[120, 80, 95], # rows: 2 stores
[ 60, 110, 70]]) # columns: 3 months
sales.sum(axis=0) # per month: [180 190 165]
sales.sum(axis=1) # per store: [295 240]
sales / sales.sum(axis=1, keepdims=True) # each store's monthly share30. Filter an array with a conditionEasy
A comparison on an array gives a boolean mask, an array of True and False, which you can use to select values. np.where picks between two values element by element, and the mean of a mask is the share that’s True.
amounts = np.array([1798, 2499, 1999, 3398, 1299])
amounts[amounts > 2000] # array([2499, 3398])
np.where(amounts > 2000, "High", "Normal")
(amounts > 2000).mean() # 0.4: 2 of 5Coding tasks: 5 questions
Short problems of the kind set in online tests and live rounds. Say the approach and its cost before you code.
31. Remove duplicates from a list, keeping the orderEasy
set() removes duplicates but loses the order. Dicts keep insertion order (a language rule since Python 3.7), so dict.fromkeys does it in one line; the loop shows you understand why.13
def dedupe(items):
return list(dict.fromkeys(items))
def dedupe_loop(items):
seen, out = set(), []
for x in items:
if x not in seen:
seen.add(x)
out.append(x)
return out
dedupe(["Pune", "Delhi", "Pune", "Jaipur", "Delhi"]) # ['Pune', 'Delhi', 'Jaipur']32. Count the words in a text and show the top 3Easy
Lower-case the text, pull out the words with a regular expression (so punctuation doesn’t stick to them), then count with Counter. most_common breaks ties in the order the words first appeared.11
import re
from collections import Counter
text = "UPI is fast. UPI is free. Cards are not free."
words = re.findall(r"[a-z']+", text.lower())
Counter(words).most_common(3) # [('upi', 2), ('is', 2), ('free', 2)]33. Find two products whose prices add up to a gift card’s valueMedium
The classic “two sum”. Checking every pair takes time that grows with the square of the list. A dict of prices seen so far does it in one pass: for each price, look up the partner you need.
def pair_with_sum(prices, target):
seen = {} # price -> position
for i, p in enumerate(prices):
if target - p in seen:
return seen[target - p], i
seen[p] = i
return None
pair_with_sum([499, 1299, 1899, 2499], 3798) # (1, 3): 1299 + 249934. The top 2 orders by amount in each cityMedium
Sort, then take the first rows of each group; or rank inside each group and filter, which mirrors SQL’s DENSE_RANK and keeps ties. NaN sorts last, so the missing amount never makes the top.
top2 = (orders.sort_values("amount", ascending=False)
.groupby("city")
.head(2))
orders["rank_in_city"] = (orders.groupby("city")["amount"]
.rank(method="dense", ascending=False))
orders[orders["rank_in_city"] <= 2]35. Read a messy CSV and clean itHard
The most common take-home task. Work in a fixed order: tidy the column names, parse dates with their format, clean the text, turn amounts into numbers, then remove duplicates, since rows that differ only in spacing or case match once they’re cleaned. Finish with info() to check every type.
import io
raw = io.StringIO("""Order ID, Order Date ,City,Amount
1001,02/09/2026, pune ,"₹1,798"
1002,05/09/2026,Ahmedabad,"₹2,499"
1002,05/09/2026,Ahmedabad,"₹2,499"
1003,20/09/2026,PUNE,
""")
df = pd.read_csv(raw)
df.columns = df.columns.str.strip().str.lower().str.replace(" ", "_")
df["order_date"] = pd.to_datetime(df["order_date"], format="%d/%m/%Y")
df["city"] = df["city"].str.strip().str.title()
df["amount"] = pd.to_numeric(df["amount"].str.replace(r"[₹,]", "", regex=True),
errors="coerce")
df = df.drop_duplicates()
df.info() # 3 rows; order_date datetime64, amount float64 with one NaNA 7-day practice plan
About 90 minutes a day. Type every answer yourself in a notebook before you read ours, and predict each output before you run it.
| Day | Focus | Questions | Finish when |
|---|---|---|---|
| 1 | Types, copies and the default-argument trap | 1–5 | You can predict what each snippet prints |
| 2 | Comprehensions, functions, lambdas, generators | 6–9, 31–32 | You can write a generator from memory |
| 3 | Errors, classes, decorators, dicts | 10–13, 33 | Two-sum in one pass, explained out loud |
| 4 | pandas basics: selecting, filtering, counting, grouping | 14–19 | loc and iloc without a second thought |
| 5 | Merges, reshaping, vectorising, missing values | 20–23 | Every merge checked with validate |
| 6 | Dates, windows, copy-on-write, memory, NumPy | 24–30 | You can explain axis and broadcasting |
| 7 | A mock take-home: clean and analyse a public dataset in 60 minutes | 34–35 | A clean notebook with three findings in plain words |
For day 7, pick a dataset from our 15 data analyst projects on Indian public data: real files are messier than any example, which is the point.
Free, official practice material: The Python Tutorial, the pandas User Guide, NumPy: the absolute basics, and Kaggle Learn’s Python and Pandas courses.
Questions
Is Python required for a data analyst job?
Not for every role: many analyst jobs centre on SQL, Excel and Power BI. Python, mainly pandas, widens the roles you can apply for, and Data Scientist roles expect it.
How much Python do I need for a data analyst interview?
Enough to load a file, clean it, group and join it, and explain what you found: questions 1–8 and 14–24 in this guide. Classes, decorators and generators matter more for Data Scientist and AI/ML Engineer roles.
Do data interviews ask DSA or LeetCode questions?
Less than software interviews do. Expect practical problems with strings, lists and dicts, like the five coding tasks here; some product companies add an easy algorithm problem, so practise a few.
Which matters more for analysts, pandas or SQL?
Learn both: SQL gets data out of company databases, and pandas cleans and analyses it once it’s out. Most analyst rounds test SQL, and many add a pandas task.
Which pandas version should I learn in 2026?
pandas 3.0, the current release. It made copy-on-write the only mode and a new str dtype the default for text, so older tutorials that rely on chained assignment or object-dtype strings will mislead you.
Sources
- pandas 3.0 documentation, Copy-on-Write (CoW), read 17 Oct 2026.
- pandas documentation, What’s new in 3.0.0, read 17 Oct 2026.
- pandas 3.0 documentation, Indexing and selecting data, read 17 Oct 2026.
- pandas 3.0 documentation, Group by: split-apply-combine, read 17 Oct 2026.
- pandas 3.0 documentation, Merge, join, concatenate and compare, read 17 Oct 2026.
- pandas 3.0 documentation, Working with missing data, read 17 Oct 2026.
- pandas 3.0 documentation, Time series / date functionality, read 17 Oct 2026.
- pandas 3.0 documentation, Scaling to large datasets, read 17 Oct 2026.
- NumPy documentation, Broadcasting, read 17 Oct 2026.
- The Python Tutorial, More Control Flow Tools (default argument values), read 17 Oct 2026.
- Python documentation, collections: Container datatypes, read 17 Oct 2026.
- Python documentation, copy: Shallow and deep copy operations, read 17 Oct 2026.
- Python documentation, Built-in Types (dictionary order), read 17 Oct 2026.
- Indeed Hiring Lab, September 2025 labour market update, 25 Oct 2025.





