Fix
Getting Started With Python Excel AutomationDeep dive

Fix openpyxl "TypeError: expected <class ...>"

openpyxl checks the type of every style, chart and dimension attribute, and raises "TypeError: expected <class 'float'>" or "expected <class 'openpyxl.styles.fills.Fill'>" when it gets the wrong kind of value. Read the class name, find the assignment, and pass the right object — including the case where the error only appears at save time, far from the line that caused it.

openpyxl models the Excel file format with typed attributes: a font size must be a number, a fill must be a Fill object, a colour must be a Color. When code assigns something else, openpyxl raises one of these:

Text
TypeError: expected <class 'float'>
TypeError: expected <class 'openpyxl.styles.fills.Fill'>
TypeError: <class 'openpyxl.styles.fonts.Font'>.color should be <class 'openpyxl.styles.colors.Color'> but value is <class 'tuple'>

The class in the message is what openpyxl wanted; the job is to find which assignment gave it something else. Usually that is the line in the traceback. Sometimes it is not: a few mistakes are accepted silently and fail only when the workbook is saved, with a traceback that points into openpyxl's writer. This page covers both. It is part of Troubleshooting Common Python Excel Errors.

Prerequisites

Bash
pip install openpyxl

The examples use openpyxl 3.1. Each one reproduces the error, then shows the fix.

How openpyxl checks a value A value is assigned to an attribute. openpyxl's descriptor tries to convert it to the expected type. If conversion succeeds the value is stored; if it fails, a TypeError names the expected class. Some assignments bypass the check and fail when the file is written. Assignment font.size = 'large' Descriptor expected type Convert float('large') Fails TypeError names the class Or at save stylesheet writer

Case 1 — expected <class 'float'>: a number that is not a number

Numeric attributes such as font size, column width, row height and chart style try to convert the value with float(). A numeric string converts; a word does not:

Python
from openpyxl import Workbook
from openpyxl.styles import Font

wb = Workbook()
ws = wb.active

for label, action in [
    ("Font(size='12')", lambda: Font(size="12")),
    ("Font(size='large')", lambda: Font(size="large")),
    ("column width 'wide'", lambda: setattr(ws.column_dimensions["A"], "width", "wide")),
    ("column width None", lambda: setattr(ws.column_dimensions["A"], "width", None)),
]:
    try:
        action()
        print(f"{label:22} accepted")
    except TypeError as e:
        print(f"{label:22} TypeError: {e}")

The values usually come from a config file or a spreadsheet of settings, where "12" is fine and "large" or an empty cell is not. Validate settings when you load them, not when openpyxl rejects them. Resetting a column to its default width is del ws.column_dimensions["A"], not width = None.

Case 2 — should be <class 'Color'> but value is <class 'tuple'>

Colours are the most common source. openpyxl accepts a Color object or an aRGB hex string: six hex digits (FF0000) or eight with alpha (FFFF0000). RGB tuples raise a TypeError; CSS-style strings with # and colour names raise a ValueError:

Python
from openpyxl.styles import PatternFill

for label, action in [
    ("Font(color=(255, 0, 0))", lambda: Font(color=(255, 0, 0))),
    ("Font(color='#FF0000')", lambda: Font(color="#FF0000")),
    ("PatternFill(fgColor='red')", lambda: PatternFill("solid", fgColor="red")),
    ("Font(color='FF0000')", lambda: Font(color="FF0000")),
]:
    try:
        action()
        print(f"{label:28} accepted")
    except (TypeError, ValueError) as e:
        print(f"{label:28} {type(e).__name__}: {str(e)[:70]}")

A small converter keeps colour handling in one place and accepts whatever the rest of your code uses:

Python
def to_argb(color):
    if isinstance(color, tuple):
        return "{:02X}{:02X}{:02X}".format(*color)
    return color.lstrip("#").upper()

print(Font(color=to_argb((255, 0, 0))).color.rgb, Font(color=to_argb("#1f4e79")).color.rgb)

Case 3 — the error appears when saving

This is the confusing one. Assigning a string to a cell's fill is accepted without complaint, and the TypeError only appears at wb.save(), with a traceback that runs through openpyxl's stylesheet writer and never mentions your code:

Python
import traceback

wb = Workbook()
ws = wb.active
ws["A1"] = "Total"
ws["A1"].fill = "FFFF00"          # accepted here...
try:
    wb.save("report.xlsx")         # ...fails here
except TypeError as e:
    frames = [f"{f.filename.split('site-packages/')[-1]}:{f.name}" for f in traceback.extract_tb(e.__traceback__)]
    print("TypeError:", e)
    print(" -> ".join(frames[-4:]))

When the traceback ends in write_stylesheet or elsewhere in openpyxl/writer, search your code for style assignments — .fill =, .border =, .font =, .alignment =, .protection = — and check that each assigns a style object. The fix is the object, but there is a catch: correcting the cell in the same session is not enough. The bad value was already added to the workbook's table of fills, and the next save fails the same way:

Python
ws["A1"].fill = PatternFill(fill_type="solid", fgColor="FFFF00")
try:
    wb.save("report.xlsx")
except TypeError as e:
    print("still fails:", e)

Fix the code and rebuild the workbook from the start, which in a script simply means running it again:

Python
wb = Workbook()
ws = wb.active
ws["A1"] = "Total"
ws["A1"].fill = PatternFill(fill_type="solid", fgColor="FFFF00")
wb.save("report.xlsx")
print("saved")

In a large script, finding the bad assignment by reading code can take a while. Scan the workbook before saving instead: every style attribute should be a style object, and style objects have a tagname, which plain strings and numbers do not:

Python
def bad_styles(wb):
    for sheet in wb.worksheets:
        for row in sheet.iter_rows():
            for cell in row:
                for attr in ("font", "fill", "border", "alignment", "protection"):
                    if getattr(getattr(cell, attr), "tagname", None) is None:
                        yield sheet.title, cell.coordinate, attr, repr(getattr(cell, attr))

wb = Workbook()
ws = wb.active
ws["B2"], ws["C3"] = "x", "y"
ws["B2"].fill = "FFFF00"
ws["C3"].border = "thin"
print(list(bad_styles(wb)))

It names the sheet, the cell and the attribute, which is everything the save-time traceback leaves out. It cannot catch Case 4 below, where the wrong value has already been turned into a real, but wrong, style object. Run it in tests or just before save() while debugging.

When each mistake is caught A table of common wrong assignments and when openpyxl reports them. Font size given as a word fails immediately. A colour given as a tuple fails immediately. A fill given as a string is accepted and fails at save. A font given as a string is accepted and silently becomes a font named bold. Assignment Reported Font(size='large') immediately Font(color=(255, 0, 0)) immediately cell.fill = 'FFFF00' at save cell.font = 'bold' never Why fixing the cell is not enough Three layers. The cell refers to an entry in the workbook's style table. The bad string was added to that table when it was assigned. Correcting the cell adds a good entry but leaves the bad one in the table, so the stylesheet still fails to write. Cell A1 now points to a PatternFill Workbook fill table [default, 'FFFF00', PatternFill] Stylesheet writer fails on the string entry

Case 4 — the mistake that never errors

Worse than a late error is none at all. cell.font = "bold" is accepted and saved, because the string is quietly converted into a font whose name is "bold". The text is not bold; the file just asks for a font that does not exist:

Python
from openpyxl import load_workbook

wb = Workbook()
ws = wb.active
ws["A1"] = "Heading"
ws["A1"].font = "bold"
wb.save("silent.xlsx")
font = load_workbook("silent.xlsx").active["A1"].font
print(f"name={font.name!r} bold={font.b}")

Style attributes take style objects: Font(bold=True), PatternFill(...), Border(...), Alignment(...). A quick way to catch these is to assert on the style you meant in a test, for example assert ws["A1"].font.b, as shown in Test Excel Output with pytest.

Case 5 — chart attributes

Chart titles accept a string, which openpyxl converts to a Title object, but not a number; that surfaces as a "should be " error when a year or ID is used as a title. Chart style is a style number from 1 to 48, not a name:

Python
from openpyxl.chart import BarChart

chart = BarChart()
for label, action in [
    ("title = 'Sales 2026'", lambda: setattr(chart, "title", "Sales 2026")),
    ("title = 2026", lambda: setattr(chart, "title", 2026)),
    ("style = 'modern'", lambda: setattr(chart, "style", "modern")),
    ("style = 10", lambda: setattr(chart, "style", 10)),
]:
    try:
        action()
        print(f"{label:22} accepted")
    except TypeError as e:
        print(f"{label:22} TypeError: {str(e)[:80]}")

Convert labels with str() when they come from data: chart.title = str(year).

Values from pandas and NumPy

Settings often arrive from a DataFrame, as NumPy numbers rather than Python ones. Numeric attributes accept them, because float() converts them. Cell values accept NumPy scalars too. What fails is a container: a list or dict read from JSON config and assigned as a cell value raises a different error, a ValueError, not a TypeError:

Python
import numpy as np

wb = Workbook()
ws = wb.active
ws.row_dimensions[1].height = np.float32(24)
ws["A1"] = np.int64(5)
print("NumPy values accepted:", ws.row_dimensions[1].height, ws["A1"].value)
try:
    ws["A2"] = ["North", "South"]
except ValueError as e:
    print("ValueError:", e)

That one is covered in its own page, linked below; the fix is to write one value per cell or join the list into a string.

Common pitfalls

  • Reading only the last line. The class in the message tells you the kind of attribute; the traceback line above it tells you which assignment.
  • Tracebacks ending in openpyxl's writer. The cause is an earlier style assignment of the wrong type; search for .fill = and friends.
  • CSS colours. Strip the #; openpyxl wants aRGB hex, not #RRGGBB or colour names.
  • Strings for style objects. cell.font = "bold" saves without error and does nothing useful.
  • Config values as strings. Numeric strings convert, words and empty values do not; validate settings up front.

Conclusion

The class named in openpyxl's TypeError is the type the attribute expects: float for sizes, widths and chart styles, Color for colours, Fill, Font and the other style classes for cell formatting, Title for chart titles. Pass that type — style objects rather than strings, aRGB hex strings rather than tuples or # colours, numbers rather than words. When the traceback ends inside openpyxl's writer, the cause is an earlier style assignment; and remember that cell.font = "bold" fails silently, so check styles in tests.

Frequently asked questions

What does "TypeError: expected <class 'float'>" mean in openpyxl? A numeric attribute such as a font size, column width, row height or chart style received a value that float() cannot convert, such as a word or None. Pass a number.

How do I set a colour in openpyxl? Use an aRGB hex string without the hash, such as "FF0000" or "FFFF0000", or a Color object. RGB tuples and CSS names are rejected.

Why does openpyxl raise TypeError only when I save? Some style assignments, such as a string assigned to cell.fill, are stored without checking and converted when the stylesheet is written. Look for style attributes assigned a string or other plain value.

Why is my text not bold after cell.font = "bold"? The string is treated as a font name, so the cell gets a font called bold. Use cell.font = Font(bold=True).

More on errors & fixes

More on errors & fixes →