This is the toolkit that turns Python from a calculator into a data language. Four ideas do most of the work in everyday analysis: drawing charts with Matplotlib, storing labeled data in dictionaries, working with tables through pandas DataFrames, and repeating work with loops. None of them is complicated on its own, and together they cover a surprising fraction of real data work. This article walks through all four, with the small decisions that separate code that works from code that reads well.
Matplotlib: Picking the Right Chart
Matplotlib is Python’s drawing tool, and the first decision is always which chart answers your question. A line plot connects points in order, which makes it the natural choice when the x-axis is time and you want to see how something changed. A scatter plot drops points without connecting them, which is what you want when asking whether two variables are related. A histogram chops a single variable into bins and counts how many values land in each, which reveals the shape of a distribution.
import matplotlib.pyplot as pltplt.plot(x, y) # line: trends over timeplt.scatter(x, y) # scatter: relationship between two variablesplt.hist(data, bins=10) # histogram: distribution of one variableplt.show() # always last: actually render the figureplt.clf() # clear the canvas before the next plot
Everything before plt.show() is just describing the picture; that call is the moment the window actually opens and you see it. And plt.clf() is the eraser, clearing the canvas so the next chart starts fresh rather than drawing on top of the last one.
A chart without labels is a riddle, so the next layer is the explanatory text. Axis labels and a title say what the viewer is looking at, and a few other touches handle the awkward cases.
plt.xlabel("Income per Person [in USD]")plt.ylabel("Life Expectancy [in years]")plt.title("Global Development, 2021")plt.xscale("log")plt.xticks([1000, 10000, 100000], ["1k", "10k", "100k"])plt.text(1550, 71, "Kenya")plt.grid(True)
The log scale earns its place whenever data spans many orders of magnitude. Incomes that run from a few hundred dollars to tens of thousands get squashed into the left margin on a linear axis, but a logarithmic axis (1, 10, 100, 1000) spreads them out evenly. The xticks call then takes manual control of where the labels sit, text drops a label at a specific coordinate to call out one point, and grid(True) adds the faint background lines that make values easier to read off.
A scatter plot has a hidden talent: it can show more than two variables at once by varying each dot’s size, color, and transparency.
plt.scatter(x, y, s = pop_sizes * 2, # dot size, from a population array c = region_colors, # dot color, by region alpha = 0.8 # transparency, 0 invisible to 1 solid)
Mapping population to s turns dot size into a third variable, so larger countries become larger dots. Coloring by region with c adds a fourth. And alpha controls transparency, which matters when many points overlap, because semi-transparent dots blend and reveal density instead of hiding behind each other. A single scatter plot can comfortably show four dimensions this way.
Dictionaries: Lookup by Name, Not by Position
A list answers “what is at position 3?” A dictionary answers “what value goes with this name?” It is a phone book: each entry pairs a key you look up with a value you get back.
capitals = { "japan": "tokyo", "brazil": "brasilia", "canada": "ottawa"}capitals["japan"] # "tokyo"capitals.keys() # all the keyscapitals["egypt"] = "cairo" # add a new entrycapitals["canada"] = "ottawa" # update an existing one"egypt" in capitals # Truedel capitals["brazil"] # remove an entry
Curly braces create the dictionary, a colon separates each key from its value, and lookups use the same square-bracket syntax as lists except you pass a key instead of a number. Adding and updating look identical because Python simply checks whether the key already exists. The in keyword is a fast membership test, and del removes an entry outright. Reach for a dictionary whenever your data has natural names rather than an order.
When each entry needs to carry several facts, you nest a dictionary inside a dictionary.
capitals = { "japan": {"capital": "tokyo", "population": 125.7}, "brazil": {"capital": "brasilia", "population": 214.3}}capitals["brazil"]["capital"] # "brasilia"capitals["brazil"]["population"] # 214.3
Now capitals["brazil"] returns another small dictionary rather than a single string, and you chain a second lookup to dig into it. This mirrors real-world structure far better than inventing flat keys like "brazil_capital"; the nesting follows the hierarchy of the data itself.
DataFrames: Spreadsheets Inside Python
A pandas DataFrame is a spreadsheet living in code: rows are observations, columns are variables. Building one from a dictionary is intuitive, because each key becomes a column name and each value (a list) becomes that column’s data.
import pandas as pddata = { "country": ["US", "Australia", "Japan"], "vehicles_per_cap": [809, 731, 588], "drives_on_right": [True, False, False]}transport = pd.DataFrame(data)
The lists must be the same length so the rows line up, and pandas handles alignment, indexing, and type inference automatically. More often the data already lives in a CSV file, which pandas reads in a single line.
transport = pd.read_csv("transport.csv")transport = pd.read_csv("transport.csv", index_col=0)
The index_col=0 argument tells pandas to treat the first column as row labels rather than data. Without it you get a default integer index; with it you might get country codes like “US” or “JPN” as labels, which makes name-based lookups possible later.
Selecting columns has one subtlety worth internalizing. Single brackets return a Series, a one-dimensional labeled array. Double brackets return a DataFrame, which stays two-dimensional and is also how you grab several columns at once.
transport["country"] # Series (one column)transport[["country"]] # DataFrame (one column)transport[["country", "drives_on_right"]] # DataFrame (multiple columns)
The rule of thumb: single brackets mean “give me the contents,” double brackets mean “give me a DataFrame containing these columns.”
You can slice rows by position with the same syntax lists use, where transport[0:3] grabs the first three rows and the end index is exclusive. But that approach cannot easily combine row and column selection, which is why pandas provides .locand .iloc. The difference between them is how they point at things: .loc uses labels, the names you actually see, while .ilocuses integer positions.
transport.loc["JPN"] # one row by labeltransport.loc[["AUS", "EG"]] # several rowstransport.loc["MOR", "drives_on_right"] # one celltransport.loc[["RU", "MOR"], ["country", "drives_on_right"]] # rows and columnstransport.iloc[0] # first row by positiontransport.iloc[:, 1] # all rows, second columntransport.iloc[0:3, 0:2] # rows 0 to 2, columns 0 to 1
The syntax is always [rows, columns]: first which rows, then which columns, with a colon meaning “all of them” and a list meaning “these specific ones.” So the fourth line reads as “give me the rows labeled RU and MOR, and only the country and drives_on_right columns.” Once this clicks, you can extract any sub-rectangle of a table you need, which is the workhorse skill of DataFrame work.
Comparisons and Boolean Logic
Comparison operators ask yes-or-no questions and return True or False. The classic trap is == versus =: a single equals assigns a value to a variable, while a double equals asks whether two values are the same. The rest map to ordinary math symbols (!=, >, <, >=, <=), and every comparison produces a boolean that becomes the input to an if statement or a filter.
Combining those questions uses and, or, and not. The and operator requires both parts to be true, like a job that demands a degree and experience; or requires at least one; not flips a truth value. These three can express any logical condition you can describe.
There is one important exception. Plain and and or work on a single boolean, but they break on a whole NumPy array of booleans, because Python cannot tell whether you mean “all of them” or “any of them.” NumPy provides element-wise versions instead.
import numpy as npnp.logical_and(arr > 10, arr < 20) # True where both holdnp.logical_or(arr > 18, arr < 5) # True where either holdsnp.logical_not(arr > 10) # flips each element
Each takes arrays of booleans and returns a new array, position by position, which is how you build filters like “values between 10 and 20” across an entire array at once.
Conditionals
Conditionals let code make decisions. if poses the first question and runs its indented block if true; elif (else-if) offers an alternative question when the previous ones were false; else is the catch-all when nothing matched. Only one branch ever runs, because Python stops at the first true condition.
area = 14.0if area > 15: print("big place!")elif area > 10: print("medium size!")else: print("pretty small.")
Tracing it: area is 14, so area > 15 is false and gets skipped, area > 10 is true so “medium size!” prints, and the else never runs. Order matters here in a way that bites people. If you checked area > 10 first, a value of 16 would print “medium size!” because that branch would catch it before the area > 15 check ever ran. Order conditions from most specific to least.
Loops
A while loop repeats as long as a condition stays true, rechecking it before every pass and exiting the moment it turns false.
offset = 8while offset != 0: offset -= 1 print(offset)
The lurking danger is the infinite loop: if nothing inside moves the condition toward false, it never ends. Here offset counts down from 8 and the loop stops when it hits 0. When the starting value could be positive or negative, you branch inside the loop to always step toward the exit.
while offset != 0: if offset > 0: offset -= 1 else: offset += 1
Whether offset begins above or below zero, each pass brings it one step closer, so the loop always terminates.
A for loop visits each item in a sequence in turn, and it comes in several handy variations.
for item in my_list: print(item)for index, item in enumerate(my_list): # value plus its position print(index, item)for name, area in rooms: # unpack each [name, area] pair print(name, area)for key, value in my_dict.items(): # keys and values together print(key, value)
Plain iteration hands you one element per pass. enumerate adds the position number, so you know “this is the third item.” For nested data like a list of pairs, you can unpack each sublist into separate variables right in the loop header. And for dictionaries, plain iteration gives you only the keys, so .items() is the trick that gives you keys and values together.
DataFrames need special handling, because looping over one directly gives you column names, which is rarely what you want. The iterrows() method walks the rows, handing back each row’s label and its data as a Series.
for label, row in transport.iterrows(): print(label) # the row index, e.g. "JPN" print(row["country"]) # one value from that row
This is convenient and readable, which makes it ideal for learning and small jobs, though it is slow on large DataFrames where vectorized operations win easily. You can use it to build a new column row by row:
for label, row in transport.iterrows(): transport.loc[label, "COUNTRY"] = row["country"].upper()
For each row this grabs the country name, uppercases it, and writes the result into a new “COUNTRY” column using .locwith the row label and the new column name. After the loop, the column is fully populated. For real work the same result comes from a single vectorized line, transport["COUNTRY"] = transport["country"].str.upper(), which runs far faster, but the loop version is the clearer way to understand what is happening underneath.
Quick Reference
| Task | Code |
|---|---|
| Line plot | plt.plot(x, y) |
| Scatter plot | plt.scatter(x, y) |
| Histogram | plt.hist(data, bins=10) |
| Show / clear | plt.show() / plt.clf() |
| Log scale | plt.xscale("log") |
| Annotate a point | plt.text(x, y, "label") |
| Create a dict | {"key": "value"} |
| Access / add | d["key"] / d["key"] = value |
| Loop a dict | for k, v in d.items(): |
| Dict to DataFrame | pd.DataFrame(data) |
| CSV to DataFrame | pd.read_csv("f.csv", index_col=0) |
| Column / columns | df["col"] / df[["a", "b"]] |
| Rows by label | df.loc["label"] |
| Rows by position | df.iloc[0:3] |
| One cell | df.loc["row", "col"] |
| while / for | while cond: / for x in seq: |
| Loop with index | for i, v in enumerate(lst): |
| Loop a DataFrame | for label, row in df.iterrows(): |
| NumPy and / or | np.logical_and(a, b) / np.logical_or(a, b) |
Conclusion
These four tools form the backbone of everyday Python data work. Matplotlib draws the chart that matches your question, line for trends, scatter for relationships, histogram for distributions. Dictionaries store data you look up by name, and nest cleanly when each entry needs several facts. DataFrames bring spreadsheet thinking into code, with .loc and .iloc as the precise way to carve out any rows and columns you need. And loops repeat work, with for walking sequences, whilerunning until a condition flips, and iterrows() handling DataFrames when a readable loop beats a fast one. Master these and most introductory data tasks become a matter of combining pieces you already understand.
[…] Python Fundamentals: Plots, Dictionaries, DataFrames and Loops […]
[…] Python fundamentals article gathers the four tools that turn Python into a data language: matplotlib for seeing data, […]
[…] the full background, read the guide to Python fundamentals: plots, dictionaries, DataFrames, and loops. To practise, work through the 10 code-along […]