Publication figures need fixed physical dimensions, readable typography, and output formats that match where the graphic will be placed. Matplotlib can produce vector files for manuscript and layout tools while also writing a high-DPI PNG for systems that require raster artwork.
The export path starts with the figure size in inches because Matplotlib uses inches as its native figure unit. A 3.5 inch by 2.4 inch figure saved at 300 DPI becomes a 1050 by 720 pixel PNG, while PDF and SVG keep the same physical page size as vector output.
Set typography, layout, and metadata in the same script that creates the plot so notebook display settings do not become the submission artifact. Inspect the saved files after export, because bbox_inches='tight', different DPI values, or viewer-side scaling can change the file that reaches a journal, slide deck, or documentation page.
Related: How to set figure size in Matplotlib
Related: How to save a Matplotlib figure
Figure size: 3.50 x 2.40 in Raster DPI: 300 Vector output: PDF, SVG Raster output: PNG
PDF and SVG stay editable as vector artwork in most layout tools. PNG uses the figure size multiplied by DPI, so a 3.50 inch wide figure at 300 DPI exports as 1050 pixels wide.
Related: How to set figure size in Matplotlib
from pathlib import Path import re import xml.etree.ElementTree as ET import matplotlib import matplotlib.pyplot as plt import numpy as np from PIL import Image OUT = Path("exports") OUT.mkdir(exist_ok=True) width_in = 3.50 height_in = 2.40 dpi = 300 plt.rcParams.update( { "font.size": 8, "axes.labelsize": 8, "axes.titlesize": 9, "legend.fontsize": 7, "xtick.labelsize": 7, "ytick.labelsize": 7, "pdf.fonttype": 42, "ps.fonttype": 42, "svg.fonttype": "none", } ) days = np.arange(1, 7) control = np.array([2.1, 2.4, 2.8, 3.0, 3.4, 3.7]) treatment = np.array([2.0, 2.7, 3.4, 4.1, 4.6, 5.0]) fig, ax = plt.subplots(figsize=(width_in, height_in), dpi=dpi, layout="constrained") ax.plot(days, control, marker="o", linewidth=1.4, label="Control") ax.plot(days, treatment, marker="s", linewidth=1.4, label="Treatment") ax.set_title("Response over time") ax.set_xlabel("Day") ax.set_ylabel("Mean response") ax.grid(True, linewidth=0.4, alpha=0.35) ax.legend(frameon=False) metadata = { "Title": "Publication figure export", "Author": "Data Team", "Creator": "Matplotlib publication export script", } pdf = OUT / "publication-figure.pdf" svg = OUT / "publication-figure.svg" png = OUT / "publication-figure.png" fig.savefig(pdf, metadata=metadata) fig.savefig(svg, metadata={"Title": metadata["Title"], "Creator": metadata["Creator"]}) fig.savefig(png, dpi=dpi, metadata={"Title": metadata["Title"]}) plt.close(fig) def pdf_size_inches(path): match = re.search( rb"/MediaBox\s*\[\s*0\s+0\s+([0-9.]+)\s+([0-9.]+)\s*\]", path.read_bytes(), ) if not match: raise RuntimeError(f"MediaBox not found in {path}") return float(match.group(1)) / 72, float(match.group(2)) / 72 def svg_unit_to_inches(value): if value.endswith("pt"): return float(value[:-2]) / 72 if value.endswith("in"): return float(value[:-2]) raise RuntimeError(f"Unsupported SVG unit: {value}") def svg_size_inches(path): root = ET.parse(path).getroot() return svg_unit_to_inches(root.attrib["width"]), svg_unit_to_inches( root.attrib["height"] ) def print_inches(label, path, size): print(f"{path.name:<24} {label:<3} {size[0]:.2f} x {size[1]:.2f} in") with Image.open(png) as image: png_size = image.size png_title = image.text.get("Title", "") print(f"matplotlib {matplotlib.__version__} ({matplotlib.get_backend()} backend)") print(f"target {width_in:.2f} x {height_in:.2f} in at {dpi} dpi") print_inches("PDF", pdf, pdf_size_inches(pdf)) print_inches("SVG", svg, svg_size_inches(svg)) print(f"{png.name:<24} PNG {png_size[0]} x {png_size[1]} px") print(f"png title metadata {png_title}")
layout=“constrained” lets Matplotlib allocate room for labels, ticks, title, and legend before saving. The font settings keep text readable after the figure is placed at its final width.
Related: How to fix overlapping labels in Matplotlib
Related: How to configure fonts in Matplotlib
Use bbox_inches='tight' only when trimmed whitespace matters more than exact page dimensions, because tight bounding can change the saved canvas size.
$ python publication_figure_export.py matplotlib 3.11.0 (Agg backend) target 3.50 x 2.40 in at 300 dpi publication-figure.pdf PDF 3.50 x 2.40 in publication-figure.svg SVG 3.50 x 2.40 in publication-figure.png PNG 1050 x 720 px png title metadata Publication figure export
The PDF and SVG rows should match the target inches. The PNG row should match the target inches multiplied by DPI.