To add text to a plot in Matplotlib, call plt.text(x, y, "text") or ax.text(x, y, "text"), where x and y are the position in data coordinates. Add bbox=dict(...) to put the text in a box, transform=ax.transAxes to place it relative to the axes, fig.text() to write outside the plot, and annotate() to point at a value with an arrow. This guide shows each way to add text to a plot in Python with real Matplotlib windows.
Tested with Python 3.12.5 and Matplotlib 3.11.2. The screenshots are real Matplotlib windows and the Windows Command Prompt. Reference: Axes.text, annotations guide.
Add text with plt.text()
import matplotlib.pyplot as plt
months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun"]
sales = [120, 135, 128, 160, 172, 190]
plt.plot(months, sales, marker="o")
plt.text(3, 150, "New store opened", fontsize=12, color="darkgreen") # x, y in data coordinates
plt.title("Monthly sales")
plt.show()

plt.text(3, 150, ...): x = 3 is the fourth category (Apr), y = 150 on the sales axis.With categorical x values like month names, the positions are 0, 1, 2 …, so x=3 is April.
Where the text goes: data, axes and figure coordinates
By default the position is in data coordinates and the text moves when the data or limits change. Two other coordinate systems are often more useful:
import matplotlib.pyplot as plt
import numpy as np
x = np.linspace(0, 10, 100)
fig, ax = plt.subplots(figsize=(7, 4.5))
ax.plot(x, np.sin(x))
ax.set_ylim(-1.3, 1.6)
ax.text(8, -0.9, "data coords (8, -0.9)", ha="center") # moves with the data
ax.text(0.02, 0.95, "axes coords (0.02, 0.95)", transform=ax.transAxes, va="top") # fixed corner of the axes
fig.text(0.5, 0.01, "figure coords: centred under the plot", ha="center",
style="italic", color="gray") # outside the axes
plt.subplots_adjust(bottom=0.15)
plt.show()

text() call in three coordinate systems.| Coordinates | Code | (0, 0) is | (1, 1) is |
|---|---|---|---|
| Data (default) | ax.text(x, y, s) | the data point 0, 0 | the data point 1, 1 |
| Axes | ax.text(x, y, s, transform=ax.transAxes) | bottom-left of the plot area | top-right of the plot area |
| Figure | fig.text(x, y, s) | bottom-left of the whole figure | top-right of the whole figure |
To add text outside the plot, use fig.text(), or axes coordinates beyond 0 to 1 (for example x=1.02 for the right side), and leave room with subplots_adjust() or tight_layout().
Add a text box
Pass a bbox dictionary to draw a box behind the text. Combined with axes coordinates, it makes a statistics box that stays in the corner:
import matplotlib.pyplot as plt
import numpy as np
rng = np.random.default_rng(0)
scores = rng.normal(72, 9, 200)
fig, ax = plt.subplots(figsize=(7, 4.5))
ax.hist(scores, bins=20, color="tab:blue", alpha=0.8)
stats = f"n = {scores.size}\nmean = {scores.mean():.1f}\nstd = {scores.std():.1f}"
ax.text(0.97, 0.95, stats, transform=ax.transAxes, ha="right", va="top", fontsize=11,
bbox=dict(boxstyle="round,pad=0.5", facecolor="lightyellow", edgecolor="gray")) # text box
ax.set_xlabel("Exam score")
plt.show()

bbox=dict(boxstyle="round", ...).Text box styles, fonts, rotation and math
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(7, 4.5))
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.text(1, 8.5, "round", bbox=dict(boxstyle="round", fc="lightblue"))
ax.text(4, 8.5, "square", bbox=dict(boxstyle="square", fc="lightgreen"))
ax.text(7, 8.5, "sawtooth", bbox=dict(boxstyle="sawtooth", fc="pink"))
ax.text(1, 5, "Bold 16 pt", fontsize=16, fontweight="bold")
ax.text(5.5, 5, "Rotated 30°", rotation=30, fontsize=13, color="purple")
ax.text(1, 2, r"Math: $E = mc^2$, $\alpha^2 + \beta^2$", fontsize=14)
ax.text(6.5, 2, "Two\nlines", ha="center", va="center", fontsize=12,
bbox=dict(boxstyle="circle", fc="wheat"))
plt.show()

ha/va(horizontal / vertical alignment) decide which part of the text sits at (x, y).- Write maths between
$...$in a raw string, for exampler"$\alpha^2$". - Use
\nfor several lines.
Annotate a point with an arrow
annotate() places text at xytext and draws an arrow to the point xy:
import matplotlib.pyplot as plt
years = [2019, 2020, 2021, 2022, 2023, 2024]
users = [1.2, 0.9, 1.8, 2.9, 3.4, 4.6]
fig, ax = plt.subplots(figsize=(7, 4.5))
ax.plot(years, users, marker="o")
ax.annotate("Pandemic dip", xy=(2020, 0.9), xytext=(2020.4, 2.6),
arrowprops=dict(arrowstyle="->", color="red"), color="red", fontsize=11)
ax.annotate("Record year", xy=(2024, 4.6), xytext=(2022.2, 4.3),
arrowprops=dict(arrowstyle="-|>", connectionstyle="arc3,rad=-0.3"), fontsize=11)
ax.set_ylabel("Users (millions)")
plt.show()

ax.annotate() with different arrow styles.Label each point in a scatter plot
import matplotlib.pyplot as plt
cities = {"Chicago": (2.7, 227), "Houston": (2.3, 1651), "Phoenix": (1.6, 1340),
"Boston": (0.65, 125), "Seattle": (0.75, 217)}
fig, ax = plt.subplots(figsize=(7, 4.5))
offsets = {"Seattle": (6, 6), "Boston": (6, -12)} # nudge labels that would overlap
for name, (population, area) in cities.items():
ax.scatter(population, area, color="tab:orange")
ax.annotate(name, (population, area), xytext=offsets.get(name, (6, 4)),
textcoords="offset points", fontsize=10) # label next to each point
ax.margins(x=0.22, y=0.12) # room for labels at the edges
ax.set_xlabel("Population (millions)")
ax.set_ylabel("Area (km²)")
plt.show()

ax.annotate() call per point, offset a few points from the marker.Change or remove text later
text() returns a Text object. Keep it to update or delete the text; ax.texts and fig.texts list all texts:
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
fig, ax = plt.subplots()
t = ax.text(0.5, 0.5, "Hello", transform=ax.transAxes, fontsize=14)
fig.text(0.5, 0.02, "footer")
print("ax.texts :", [x.get_text() for x in ax.texts])
print("fig.texts:", [x.get_text() for x in fig.texts])
t.set_text("Hello, Matplotlib") # change an existing text later
t.set_color("red")
print("updated :", t.get_text(), t.get_color(), t.get_fontsize())
t.remove() # delete it
print("after remove:", len(ax.texts), "texts on the axes")
Output:
ax.texts : ['Hello']
fig.texts: ['footer']
updated : Hello, Matplotlib red 14.0
after remove: 0 texts on the axes

Interactive text input box (TextBox widget)
“Text box” can also mean an input field. Matplotlib’s TextBox widget lets the user type into the plot window; here the number typed in the box changes the frequency of the curve when Enter is pressed:
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.widgets import TextBox
x = np.linspace(0, 10, 400)
fig, ax = plt.subplots(figsize=(7, 4.5))
fig.subplots_adjust(bottom=0.2)
line, = ax.plot(x, np.sin(x))
ax.set_title("frequency = 1")
def update(text):
try:
freq = float(text) # the text typed by the user
except ValueError:
return # ignore anything that is not a number
line.set_ydata(np.sin(freq * x))
ax.set_title(f"frequency = {freq:g}")
fig.canvas.draw_idle()
box_ax = fig.add_axes([0.3, 0.05, 0.4, 0.075])
text_box = TextBox(box_ax, "Frequency: ", initial="1") # an input box inside the figure
text_box.on_submit(update) # runs when the user presses Enter
plt.show()

TextBox widget under the plot.Related Matplotlib tutorials: