Python Fundamentals: Plots, Dictionaries, DataFrames and Loops

Four tools cover most everyday Python data work: Matplotlib charts, dictionaries, pandas DataFrames, and loops. Learn how each works and exactly when to reach for it, with clear examples.

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 plt
plt.plot(x, y) # line: trends over time
plt.scatter(x, y) # scatter: relationship between two variables
plt.hist(data, bins=10) # histogram: distribution of one variable
plt.show() # always last: actually render the figure
plt.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 keys
capitals["egypt"] = "cairo" # add a new entry
capitals["canada"] = "ottawa" # update an existing one
"egypt" in capitals # True
del 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 pd
data = {
"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 label
transport.loc[["AUS", "EG"]] # several rows
transport.loc["MOR", "drives_on_right"] # one cell
transport.loc[["RU", "MOR"], ["country", "drives_on_right"]] # rows and columns
transport.iloc[0] # first row by position
transport.iloc[:, 1] # all rows, second column
transport.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 andor, 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 np
np.logical_and(arr > 10, arr < 20) # True where both hold
np.logical_or(arr > 18, arr < 5) # True where either holds
np.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.0
if 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

while loop repeats as long as a condition stays true, rechecking it before every pass and exiting the moment it turns false.

offset = 8
while 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.

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

TaskCode
Line plotplt.plot(x, y)
Scatter plotplt.scatter(x, y)
Histogramplt.hist(data, bins=10)
Show / clearplt.show() / plt.clf()
Log scaleplt.xscale("log")
Annotate a pointplt.text(x, y, "label")
Create a dict{"key": "value"}
Access / addd["key"] / d["key"] = value
Loop a dictfor k, v in d.items():
Dict to DataFramepd.DataFrame(data)
CSV to DataFramepd.read_csv("f.csv", index_col=0)
Column / columnsdf["col"] / df[["a", "b"]]
Rows by labeldf.loc["label"]
Rows by positiondf.iloc[0:3]
One celldf.loc["row", "col"]
while / forwhile cond: / for x in seq:
Loop with indexfor i, v in enumerate(lst):
Loop a DataFramefor label, row in df.iterrows():
NumPy and / ornp.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.

View Comments (3)

Leave a Reply

Subscribe to My Newsletter

Subscribe to my email newsletter to get the latest posts delivered right to your email. Pure inspiration, zero spam.

Discover more from Discuss Data Science, Machine Learning and Analytics

Subscribe now to keep reading and get access to the full archive.

Continue reading