How to Make a Histogram in Python with Matplotlib
August 11, 2026
Matplotlib will draw a histogram from one line of code. The problem is that the line you write first is almost never the one you want, because the default bin count is fixed at ten no matter what your data looks like. This walks through the working version, the settings that actually change the chart, and the two mistakes that send people to Stack Overflow.
The short version
Three lines gets you a chart on screen.
import matplotlib.pyplot as plt
plt.hist(data)
plt.show()
data here is any flat sequence of numbers: a list, a NumPy array, or a single pandas column. Not a summary table, not counts, just the raw values. If what you have is already grouped into ranges and counts, that is a frequency distribution table and it needs a different approach, covered further down.
One thing matplotlib gets right out of the box: the bars touch. A histogram shows a continuous range, so there should be no gaps between bars. Excel makes you fix that by hand, which is one of several reasons the Excel walkthrough is longer than this one.
The default that ruins most first attempts
plt.hist(data) uses ten bins. Always ten, whether you passed it thirty values or three million.
Ten is a coincidence, not a decision. With a small sample it shatters the shape into spikes. With a large one it flattens real structure into a smooth lump, and a second peak that matters can disappear entirely into a neighbouring bar.
Set the bins yourself. There are three ways, and they are useful in different situations.
Pass a number when you want a specific count:
plt.hist(data, bins=25)
Pass a list when you want to control the exact edges, which is what you need for round numbers like decades or price brackets:
plt.hist(data, bins=[0, 10, 20, 30, 40, 50])
Pass a strategy name when you would rather let a rule decide:
plt.hist(data, bins='auto')
That last one is the underrated option. Matplotlib hands the string to NumPy, which supports 'auto', 'fd', 'sturges', 'scott', 'rice', 'sqrt' and 'doane'. The 'auto' setting takes the larger of the Sturges and Freedman-Diaconis results, which is a sensible default for most real data. Those rules are not interchangeable and they can disagree by a factor of three on the same column, so it is worth knowing what each one assumes. The bin rules comparison covers Sturges, Scott and Freedman-Diaconis side by side, and how many bins a histogram should have gives the practical answer if you just want a number.
You can also clip the axis without filtering the data, which is handy when a few extreme values stretch the chart:
plt.hist(data, bins=30, range=(0, 100))
Values outside that range are dropped from the chart, not squeezed into the end bars. Worth remembering if your counts stop adding up.
Making it readable
The bare chart has no labels and the bars run together as one navy block. Four extra lines fix both.
plt.figure(figsize=(8, 5))
plt.hist(data, bins='auto', edgecolor='white')
plt.xlabel('Value')
plt.ylabel('Frequency')
plt.title('Distribution of values')
plt.show()
edgecolor is the one that makes the biggest visual difference. Without it, adjacent bars of similar height merge and you cannot see where one bin ends. White or black both work.
figsize is in inches, and it matters more than it looks. A wide, short figure exaggerates spread, a narrow tall one exaggerates peaks. If you are comparing two histograms, give them the same figsize and the same bins or the comparison is meaningless.
Density, proportions and percentages
This is where most people get a wrong answer without noticing.
density=True does not give you proportions. It scales the bars so the total area under the histogram equals 1, which means the heights are densities. With narrow bins those heights can be larger than 1, which surprises people who expected a percentage.
plt.hist(data, bins=20, density=True)
That is the right setting when you want to overlay a fitted curve, because a probability density function is on the same scale. It is the wrong setting when you want to say "18 percent of values fell in this range".
For actual proportions, weight each observation by one over the sample size:
import numpy as np
plt.hist(data, bins=20, weights=np.ones(len(data)) / len(data))
For percentages, multiply that by 100:
plt.hist(data, bins=20, weights=np.ones(len(data)) / len(data) * 100)
Now the bars sum to 1 or to 100 and mean what you would expect. The difference between counts, proportions and running totals is the same distinction covered in frequency, relative frequency and cumulative frequency, and the relative frequency histogram page has the version without any code.
For a running total, add cumulative=True.
From a pandas DataFrame
If the data is already in pandas, you do not need to pull it out first.
df['price'].hist(bins=30, edgecolor='white')
or
df.plot.hist(y='price', bins=30)
Both call matplotlib underneath, so every argument above still applies and plt.show() still displays it.
One trap: calling df.hist() on the whole frame draws a separate small histogram for every numeric column at once. That is genuinely useful for a first look at a new dataset, and confusing if you only meant to plot one column. Name the column when you mean one column.
The seaborn version
Seaborn wraps matplotlib and produces a better looking chart with less configuration.
import seaborn as sns
sns.histplot(data, bins=30, kde=True)
kde=True overlays a smoothed density curve, which helps when you are trying to judge whether a second peak is real or just bin noise. If you have seen sns.distplot in an older tutorial, it has been deprecated for years. Use histplot.
Getting the numbers, not the picture
Sometimes you want the counts rather than the chart. NumPy gives you those directly, with no figure involved.
counts, edges = np.histogram(data, bins=20)
plt.hist returns the same thing plus the bar objects, so you can draw and capture in one call:
counts, edges, patches = plt.hist(data, bins=20)
That is how you build a frequency table from raw values in code, and it is also how you check a chart that looks wrong. If a bar seems too tall, print the counts.
Saving the figure
plt.savefig('histogram.png', dpi=150, bbox_inches='tight')
Call savefig before plt.show(), not after. Showing a figure clears it on several backends, so a savefig that comes second writes a blank image. This is one of the most common "my chart saved empty" reports and the fix is just reordering two lines.
bbox_inches='tight' trims the whitespace margin, which otherwise crops axis labels in slide decks.
Two errors worth recognising
ValueError: autodetected range of [nan, nan] is not finite. Your data contains NaN. Matplotlib cannot work out an axis range from missing values. Drop them first, with df['col'].dropna() in pandas or data[~np.isnan(data)] in NumPy.
Nothing appears at all. In a plain script you need plt.show(). In an older Jupyter notebook you may need %matplotlib inline at the top, though recent versions handle it without.
When Python is the wrong tool
Python is the right choice when the histogram is a step inside something larger: a cleaning pipeline, a report you regenerate weekly, a model you are checking residuals on. The code is reproducible and it scales to millions of rows.
It is slower than it needs to be when you just want to look at a column of numbers once. Every bin change means editing a line and rerunning the cell, and sharing the result means exporting an image.
For a one-off read of some values, paste the column into the histogram maker and drag the bin slider instead. The shape updates as you move it, which is a faster way to find out whether a second peak survives a change of bins than rerunning a cell six times. There is a no-code walkthrough at how to make a histogram.
The short version, again
plt.hist(data) works but its ten-bin default is arbitrary, so pass bins='auto' or a number you chose. Add edgecolor so the bars separate and label both axes. Use density=True only when you want an area of 1, and weights when you actually want proportions or percentages. Save before you show. Then read the chart properly, because the shape is the point and how to read a histogram is the part the code cannot do for you.