How to Fix ValueError Exceptions in Python (Practical Guide)

ValueError is one of those exceptions that feels obvious after you see it, yet it still sneaks into production when inputs are messy or assumptions shift. I’ve debugged it in data pipelines, APIs, and quick scripts that were supposed to be “one‑off.” The pattern is consistent: a function receives a value that is technically the right type, but the value itself is invalid for the operation you’re asking it to do. That’s a subtle boundary, and in real systems it’s common to cross it without noticing.

I’m going to show you how I approach ValueError in day‑to‑day Python work. You’ll see the most common triggers, the fastest ways to isolate the root cause, and the fixes I actually use on modern codebases in 2026. I’ll keep the tone practical: specific input validation, clearer error messages, and defensive patterns that keep your systems reliable without making them brittle. You’ll also get runnable examples and a few “don’t do this” notes that can save you an hour of debugging.

What ValueError really means in practice

When Python raises ValueError, it’s telling you: “I got a value of the correct type, but that value is invalid for this operation.” That sounds minor, yet it’s a big difference from TypeError. For example, int(‘42‘) works because the value is valid, but int(‘fourty-two‘) fails because the string content can’t be converted. The type is still str, but the value is wrong.

I treat ValueError as a contract violation. The function you called expects a value in a certain range or format, and your code didn’t meet that contract. That’s why you’ll see it in:

  • Numeric conversion (int, float, decimal.Decimal)
  • Data parsing (datetime, json, uuid)
  • Math functions (math.sqrt, math.factorial)
  • Sequence unpacking
  • Enum conversions

The fix is rarely “catch and ignore.” The fix is almost always “validate earlier” or “convert safely” or “use the correct shape of data.”

Finding the root cause fast

When I see ValueError, I do three things immediately:

1) Reproduce the exact input

  • I log or print the raw value, not just a derived one. If a string looks like a number but includes a trailing newline or a non‑breaking space, you’ll miss it unless you inspect it directly.

2) Identify the contract boundary

  • I check the docs or the function signature to see what input range or format is expected. That’s where the invalid assumption usually lives.

3) Decide whether the fix belongs at the call site or inside the function

  • If the invalid data can appear from outside your control, validate at the boundary (HTTP requests, CSV, user input). If it’s internal logic, fix the logic upstream.

Here’s a small diagnostic pattern I use when the input is coming from a file or API:

def debug_value(value, label=‘value‘):

# Keep output explicit and inspectable

print(f‘{label}: {value!r} (type={type(value).name})‘)

raw_amount = ‘ 1,200\n‘

debugvalue(rawamount, ‘raw_amount‘)

When you use repr (!r), you’ll immediately see hidden whitespace and odd characters. That single line saves a surprising amount of time.

Common triggers and how I fix them

Invalid numeric conversions

This is the classic one. You can pass a string, and it still fails because the content isn’t a valid numeric literal.

raw_price = ‘19.99 USD‘

try:

price = float(raw_price)

except ValueError as exc:

print(f‘Invalid price format: {raw_price!r}‘)

raise

The fix is not to wrap everything in try/except. The fix is to normalize and validate the data before converting.

import re

PRICE_RE = re.compile(r‘^\s\$?(\d+(?:\.\d{1,2})?)\s$‘)

raw_price = ‘ $19.99 ‘

match = PRICERE.match(rawprice)

if not match:

raise ValueError(f‘Price must be a number with up to 2 decimals: {raw_price!r}‘)

price = float(match.group(1))

print(price)

Notice I return a clean error message with the actual value. That makes logs readable and fixes faster.

Math domain errors

Many functions in math have domain constraints. sqrt expects non‑negative values. factorial expects non‑negative integers. If you pass invalid values, ValueError appears.

import math

value = -9

try:

root = math.sqrt(value)

except ValueError:

print(‘sqrt requires a non-negative value‘)

The fix is often a guard that makes the domain explicit.

import math

def safe_sqrt(value: float) -> float:

if value < 0:

raise ValueError(f‘safe_sqrt expects value >= 0, got {value}‘)

return math.sqrt(value)

print(safe_sqrt(16))

If negative values are valid in your business logic, you might need a different function (complex numbers or a different model) rather than patching around the error.

Unpacking mismatched sequences

ValueError appears when you unpack a sequence with the wrong number of elements. This can happen when you parse CSV rows or split strings and expect a fixed shape.

row = ‘chicago,il,usa‘

city, state, country, zipcode = row.split(‘,‘)

That throws:

ValueError: not enough values to unpack (expected 4, got 3)

Fix it by either matching the expected shape or using a flexible pattern.

row = ‘chicago,il,usa‘

city, state, country = row.split(‘,‘)

Or allow extra values safely

parts = row.split(‘,‘)

city, state, country = parts[0], parts[1], parts[2]

If the shape is variable, use * unpacking:

row = ‘chicago,il,usa‘

city, state, *rest = row.split(‘,‘)

country = rest[0] if rest else ‘‘

Date and time parsing

I see ValueError from datetime.strptime more than almost any other API.

from datetime import datetime

raw_date = ‘2026-13-02‘

datetime.strptime(raw_date, ‘%Y-%m-%d‘)

The month 13 is invalid. The fix is either to validate input or to use a parsing library with clearer error reporting. In modern codebases, I prefer dateutil or pendulum for flexible parsing, but I still keep explicit validation around user input.

from datetime import datetime

raw_date = ‘2026-01-09‘

try:

parsed = datetime.strptime(raw_date, ‘%Y-%m-%d‘)

except ValueError:

raise ValueError(f‘Date must be YYYY-MM-DD, got {raw_date!r}‘)

print(parsed.date())

Enum conversions

Enums throw ValueError when you pass an unknown value. That’s a feature, not a bug.

from enum import Enum

class Environment(Enum):

DEV = ‘dev‘

PROD = ‘prod‘

raw_env = ‘production‘

Environment(raw_env) # ValueError

I usually normalize input, then check membership:

from enum import Enum

class Environment(Enum):

DEV = ‘dev‘

PROD = ‘prod‘

raw_env = ‘PROD‘

normalized = raw_env.strip().lower()

try:

env = Environment(normalized)

except ValueError:

raise ValueError(f‘Environment must be one of {[e.value for e in Environment]}‘)

Defensive patterns I rely on

Validate at the boundary

If input comes from outside your code, validate it as close to the boundary as possible. That means HTTP handlers, CLI args, file readers, and UI events. Once values pass the boundary, I treat them as trusted.

Here’s an API‑style pattern:

def parse_quantity(raw: str) -> int:

raw = raw.strip()

if not raw.isdigit():

raise ValueError(f‘Quantity must be a whole number, got {raw!r}‘)

return int(raw)

Boundary

def handle_request(params: dict) -> dict:

quantity = parse_quantity(params.get(‘quantity‘, ‘‘))

return {‘quantity‘: quantity}

This keeps ValueError contained, and your core logic stays clean.

Use explicit error messages

A generic “invalid literal” message tells you nothing about where the input came from. I always include:

  • The bad value
  • The expected format
  • The field name (if applicable)
def parse_percentage(raw: str) -> float:

try:

value = float(raw)

except ValueError:

raise ValueError(f‘percentage must be numeric, got {raw!r}‘)

if not (0 <= value <= 100):

raise ValueError(f‘percentage must be 0-100, got {value}‘)

return value

Separate conversion from validation

I like to keep conversions and validations as two steps. It avoids chaining errors and makes tests easier to write.

def to_float(raw: str) -> float:

try:

return float(raw)

except ValueError:

raise ValueError(f‘Invalid float: {raw!r}‘)

def validate_positive(value: float) -> float:

if value <= 0:

raise ValueError(f‘Value must be positive, got {value}‘)

return value

raw = ‘12.5‘

value = validatepositive(tofloat(raw))

Return structured errors in APIs

In service code, I convert ValueError into a structured response instead of a stack trace.

def api_handler(params: dict) -> dict:

try:

amount = parse_percentage(params.get(‘amount‘, ‘‘))

except ValueError as exc:

return {‘status‘: 400, ‘error‘: str(exc)}

return {‘status‘: 200, ‘amount‘: amount}

This keeps your services stable and your client messages clear.

Traditional vs modern approaches

Some patterns from earlier Python code still appear, but modern teams are shifting toward stronger input models and typed validation. Here’s how I compare them in practice:

Approach

Traditional patternModern pattern (2026)

— CLI input

int(sys.argv[1]) in placeargparse or typer with validation hooks

API parsing

Manual checks in handlerSchema models (Pydantic v2, msgspec) with explicit validators

Data parsing

try/except everywherePre-parse functions + typed dataclasses

Error reporting

Raise ValueError with generic textRaise ValueError with field and expected shape

I still use try/except, but I use it in tight, focused parts of the code rather than as a catch‑all safety net.

Real‑world scenarios and fixes I apply

CSV ingestion that fails on empty rows

I often see ValueError when ingesting CSVs with blanks or missing columns. The bug isn’t the conversion itself, it’s the assumption that every row is complete.

import csv

with open(‘orders.csv‘, newline=‘‘) as f:

reader = csv.DictReader(f)

for row in reader:

raw_total = (row.get(‘total‘) or ‘‘).strip()

if not raw_total:

continue # skip empty row

total = float(raw_total)

print(total)

This eliminates ValueError from float(‘‘) and makes the pipeline tolerant to sparse data.

User input from a web form

If your form sends strings, you will get ValueError if you assume the fields are clean.

def parse_age(raw: str) -> int:

raw = raw.strip()

if not raw.isdigit():

raise ValueError(‘age must be a non-negative integer‘)

age = int(raw)

if age > 130:

raise ValueError(‘age seems unrealistic‘)

return age

This combines validation and domain logic. If you need a different range, adjust the bound. Don’t hide it behind a generic try/except.

JSON parsing and numeric assumptions

When you parse JSON, you often get numbers as strings. If you convert without checking, ValueError shows up sporadically.

import json

def parse_payload(raw: str) -> dict:

data = json.loads(raw)

raw_score = str(data.get(‘score‘, ‘‘)).strip()

try:

score = float(raw_score)

except ValueError:

raise ValueError(f‘score must be numeric, got {raw_score!r}‘)

data[‘score‘] = score

return data

You should log the payload or field name when the conversion fails, so you can fix the upstream source if needed.

AI‑generated input

In 2026, I also see ValueError when LLMs generate semi‑structured output. When you ask a model for JSON, you may still get trailing commas or string numbers.

My fix is a strict parser plus a repair step:

import json

def safejsonload(raw: str) -> dict:

try:

return json.loads(raw)

except ValueError:

# Minimal repair strategy: strip common issues

cleaned = raw.strip().rstrip(‘,‘)

return json.loads(cleaned)

If you’re doing this often, consider schema validation and a re‑prompt in your pipeline. Don’t assume LLM output is clean just because it looks structured.

When to use try/except and when not to

I use try/except in three cases:

  • External input where conversion can fail
  • Boundary layers where you want friendly error messages
  • Very small code blocks where the exception scope is obvious

I avoid try/except for:

  • Internal logic that should be correct (fix the logic instead)
  • Broad blocks that hide the real error location
  • Situations where you can validate cheaply before calling a function

Here’s a clean pattern I recommend:

def to_int(raw: str) -> int:

raw = raw.strip()

if not raw:

raise ValueError(‘value is empty‘)

try:

return int(raw)

except ValueError:

raise ValueError(f‘invalid integer: {raw!r}‘)

And here’s a pattern I avoid:

# Avoid this: too broad, hides source

try:

x = int(data[‘x‘])

y = int(data[‘y‘])

z = int(data[‘z‘])

result = x / y + z

except ValueError:

raise ValueError(‘bad input‘)

The second example is a debugging trap. If you need multiple conversions, validate them independently or write a helper to do it field by field.

Performance considerations without false precision

ValueError handling has a cost, but it’s rarely your main bottleneck. The bigger performance impact usually comes from repeated failed conversions in large loops. I’ve measured conversion loops where a bad input rate above 5% can add a noticeable delay in ingestion pipelines, typically 10–15ms per thousand rows depending on hardware. That’s not huge, but at scale it adds up.

My approach:

  • Filter obvious invalid values before conversion
  • Use vectorized parsing when available (like pandas.to_numeric with errors=‘coerce‘)
  • Avoid try/except inside very tight loops when you can validate first

That said, correctness beats micro‑performance. If you need to catch errors to keep a pipeline running, do it, then optimize later based on profiling.

Patterns for testing ValueError fixes

I always add tests for the failure cases, not just the happy path. That’s how you prevent regressions when formats change.

import pytest

from myapp.parsers import parse_percentage

def testparsepercentage_ok():

assert parse_percentage(‘12.5‘) == 12.5

def testparsepercentage_invalid():

with pytest.raises(ValueError):

parse_percentage(‘twelve‘)

def testparsepercentageoutof_range():

with pytest.raises(ValueError):

parse_percentage(‘120‘)

If you’re using property‑based testing tools (like Hypothesis), you can generate invalid inputs automatically. That catches edge cases you wouldn’t think of, like strings with odd Unicode whitespace.

A practical checklist I use before shipping

When I fix a ValueError, I ask myself:

  • Is this input coming from outside my code?
  • Have I validated the raw input as close to the boundary as possible?
  • Do my error messages include the bad value and expected format?
  • Am I handling empty or missing values explicitly?
  • Do I have tests for common failure cases?

If I can answer “yes” to those, I’m confident the fix won’t regress when the data changes.

Deep dive: ValueError vs TypeError in real code

It’s easy to treat all input errors as the same, but distinguishing ValueError from TypeError pays off in debugging and API design.

  • TypeError: wrong type (e.g., int(None) or len(5)).
  • ValueError: right type, wrong content (e.g., int(‘x‘), math.sqrt(-1)).

Here’s a practical example:

def compute_discount(raw: str) -> float:

if raw is None:

raise TypeError(‘discount is required‘)

raw = raw.strip()

if not raw:

raise ValueError(‘discount cannot be empty‘)

try:

value = float(raw)

except ValueError:

raise ValueError(f‘discount must be numeric, got {raw!r}‘)

if not (0 <= value <= 100):

raise ValueError(‘discount must be between 0 and 100‘)

return value

This makes the error source obvious. If the input is missing, it’s a TypeError (programmer error or API misuse). If the input is present but malformed, it’s a ValueError (user or data error). That distinction helps you decide whether to fix code logic or data validation.

Hidden characters: the stealth cause of ValueError

The “looks right” input is one of the most frustrating cases. I’ve seen inputs like:

  • Numeric strings with non‑breaking spaces
  • Zero‑width joiners from copy/paste
  • Unicode minus signs instead of -

A classic example:

raw = ‘−12.5‘  # Unicode minus, not ASCII ‘-‘

float(raw) # ValueError

If you suspect this, normalize the input:

import unicodedata

def normalize_numeric(raw: str) -> str:

raw = unicodedata.normalize(‘NFKC‘, raw)

return raw.strip()

value = float(normalize_numeric(raw))

I don’t blanket‑normalize everything, but for user‑facing inputs it can save a lot of frustration.

Safer conversions for common formats

Commas and currency symbols

Human‑friendly numbers often include commas or currency symbols. You can strip them safely with a controlled approach:

import re

CURRENCY_RE = re.compile(r‘^[\s$€£]([0-9]{1,3}(?:,[0-9]{3})(?:\.[0-9]+)?|[0-9]+(?:\.[0-9]+)?)\s*$‘)

def parse_currency(raw: str) -> float:

raw = raw.strip()

match = CURRENCY_RE.match(raw)

if not match:

raise ValueError(f‘Invalid currency amount: {raw!r}‘)

numeric = match.group(1).replace(‘,‘, ‘‘)

return float(numeric)

This avoids the “just replace every comma” mistake, which can accidentally accept malformed inputs.

Percentages with symbols

Users often type 12.5%. You can parse it directly instead of making them strip the symbol.

def parse_percent(raw: str) -> float:

raw = raw.strip()

if raw.endswith(‘%‘):

raw = raw[:-1].strip()

try:

value = float(raw)

except ValueError:

raise ValueError(f‘percentage must be numeric, got {raw!r}‘)

if not (0 <= value <= 100):

raise ValueError(f‘percentage must be 0-100, got {value}‘)

return value

Booleans from user input

This isn’t ValueError directly, but it often leads to ValueError later when you assume a string is a boolean. Normalize early:

def parse_bool(raw: str) -> bool:

if raw is None:

raise ValueError(‘value is required‘)

raw = raw.strip().lower()

if raw in {‘true‘, ‘1‘, ‘yes‘, ‘y‘}:

return True

if raw in {‘false‘, ‘0‘, ‘no‘, ‘n‘}:

return False

raise ValueError(f‘Invalid boolean value: {raw!r}‘)

Managing ValueError in data pipelines

Data pipelines are where ValueError can be both common and expensive. The key is to decide whether you should:

  • Drop invalid records
  • Repair them
  • Fail fast

I often implement a “quarantine” approach where I capture invalid rows for inspection rather than dropping them silently.

import csv

bad_rows = []

with open(‘events.csv‘, newline=‘‘) as f:

reader = csv.DictReader(f)

for row in reader:

try:

ts = row[‘timestamp‘].strip()

value = float(row[‘value‘])

# Use the values for processing

except (KeyError, ValueError) as exc:

bad_rows.append({‘row‘: row, ‘error‘: str(exc)})

Later: log or store bad_rows for analysis

This approach keeps the pipeline running but preserves evidence of the failure. It’s how you fix upstream data quality issues instead of just hiding them.

Handling ValueError in APIs and CLIs gracefully

API error mapping

A clean API response avoids stack traces and helps clients fix their request:

def handlecreateuser(payload: dict) -> dict:

try:

age = parse_age(payload.get(‘age‘, ‘‘))

score = parse_percentage(payload.get(‘score‘, ‘‘))

except ValueError as exc:

return {‘status‘: 400, ‘error‘: str(exc)}

# normal flow

return {‘status‘: 201, ‘message‘: ‘ok‘}

CLI validation with argparse

Argparse doesn’t automatically validate numeric ranges, so you can plug in custom types:

import argparse

def positive_int(value: str) -> int:

try:

value = int(value)

except ValueError:

raise argparse.ArgumentTypeError(f‘invalid integer: {value!r}‘)

if value <= 0:

raise argparse.ArgumentTypeError(‘value must be positive‘)

return value

parser = argparse.ArgumentParser()

parser.addargument(‘--count‘, type=positiveint)

args = parser.parse_args()

This keeps the error experience clean while still enforcing valid inputs.

Common pitfalls (and how to avoid them)

Pitfall 1: Over‑catching ValueError

Catching ValueError broadly can hide bugs. If you have multiple conversions in one block, you lose the exact source.

Fix: keep the try/except tiny, or use a helper per field.

Pitfall 2: Using str.isdigit() too broadly

isdigit() fails for negative numbers and decimals. It also behaves oddly with some Unicode digits. That leads to ValueError later.

Fix: for integers, use int() inside a focused try/except. For floats, use float() with explicit range checks.

Pitfall 3: Assuming split() always yields the same length

Data formats change, separators are missing, or trailing commas appear. A hard unpack throws ValueError.

Fix: parse with csv when it’s CSV, and validate the number of fields when it’s custom.

Pitfall 4: Sanitizing too aggressively

If you “clean” everything by removing non‑digits, you can turn 12x3 into 123 and accept corrupted data.

Fix: sanitize cautiously and validate with a strict pattern.

Pitfall 5: Returning a generic error message

If you return “invalid input,” debugging becomes guesswork. The fix is to include the bad value and expected format in the error.

Alternative approaches for robust validation

Sometimes the best fix is not just “handle ValueError better,” but “change how you validate.” Here are options I use in modern Python projects.

Typed models (dataclasses + explicit validators)

from dataclasses import dataclass

@dataclass

class Order:

total: float

quantity: int

@classmethod

def from_raw(cls, raw: dict) -> ‘Order‘:

total = parse_currency(raw.get(‘total‘, ‘‘))

quantity = parse_quantity(raw.get(‘quantity‘, ‘‘))

return cls(total=total, quantity=quantity)

This keeps all validation in one place and makes the domain object trustworthy.

Schema validation (Pydantic / msgspec / marshmallow)

If your project already uses a schema library, use it to validate inputs before hitting business logic. The exact library doesn’t matter as much as consistent enforcement of contracts.

I still keep helper functions for custom validations, because not every rule fits a declarative schema.

Pre‑validation filters

In high‑volume pipelines, I’ll filter obvious invalid rows before the conversion step. It’s faster and avoids exceptions:

def lookslikefloat(raw: str) -> bool:

raw = raw.strip()

return bool(raw) and raw.replace(‘.‘, ‘‘, 1).isdigit()

This isn’t perfect, but it reduces the number of failures before the heavy conversion step.

Debugging ValueError in large codebases

When you don’t control every function call, ValueError can be hard to locate. I use a few debugging tricks:

Narrow the exception scope with context managers

from contextlib import contextmanager

@contextmanager

def wrapvalueerror(label: str):

try:

yield

except ValueError as exc:

raise ValueError(f‘{label}: {exc}‘)

with wrapvalueerror(‘parsing amount‘):

amount = parsecurrency(rawamount)

This adds context without losing the original exception message.

Use logging with repr()

import logging

logger = logging.getLogger(name)

try:

value = int(raw)

except ValueError:

logger.exception(‘Invalid integer input: %r‘, raw)

raise

This prints the exact value, including invisible characters, and preserves the stack trace.

Edge cases worth testing

I’ve been burned by these enough times that I proactively test them now:

  • Empty strings or strings with only whitespace
  • Strings with commas or currency symbols
  • Negative values when only positives are allowed
  • Very large numbers that overflow a business rule
  • NaN or Infinity values in floats
  • ISO dates with invalid months/days
  • Unicode spaces (non‑breaking spaces)
  • Multiple delimiters in split() output

Here’s a quick example for NaN and Infinity, which can slip through validation:

import math

def validate_finite(value: float) -> float:

if math.isnan(value) or math.isinf(value):

raise ValueError(‘value must be a finite number‘)

return value

Practical example: cleaning mixed numeric data

This example shows a realistic “messy input” case with multiple formats.

import re

import math

CLEAN_RE = re.compile(r‘[^0-9.+-]‘)

def parsemixednumber(raw: str) -> float:

if raw is None:

raise ValueError(‘value is required‘)

raw = raw.strip()

if not raw:

raise ValueError(‘value is empty‘)

# Remove common non-numeric symbols (currency, spaces)

cleaned = CLEAN_RE.sub(‘‘, raw)

try:

value = float(cleaned)

except ValueError:

raise ValueError(f‘Invalid numeric value: {raw!r}‘)

if math.isnan(value) or math.isinf(value):

raise ValueError(‘value must be finite‘)

return value

samples = [‘ $1,250.00 ‘, ‘12.5%‘, ‘-42‘, ‘‘]

for s in samples:

try:

print(parsemixednumber(s))

except ValueError as exc:

print(exc)

Notice the deliberate cleaning step. It’s limited enough to avoid accepting obviously malformed input but flexible enough for common formats. In a stricter system, I’d use a precise regex rather than generic cleanup.

Practical example: validating a CSV row with error context

import csv

def parse_row(row: dict) -> dict:

errors = []

try:

row[‘price‘] = parse_currency(row.get(‘price‘, ‘‘))

except ValueError as exc:

errors.append(f‘price: {exc}‘)

try:

row[‘quantity‘] = parse_quantity(row.get(‘quantity‘, ‘‘))

except ValueError as exc:

errors.append(f‘quantity: {exc}‘)

if errors:

raise ValueError(‘; ‘.join(errors))

return row

with open(‘items.csv‘, newline=‘‘) as f:

reader = csv.DictReader(f)

for i, row in enumerate(reader, start=1):

try:

parse_row(row)

except ValueError as exc:

print(f‘Row {i} invalid: {exc}‘)

This way you get a single ValueError per row with all the field issues at once, not just the first failure.

When ValueError should bubble up

There are cases where you should not catch ValueError at all:

  • Internal logic errors (a bug is better than a hidden failure)
  • Library code where the caller should decide how to handle the error
  • Critical transformations where corrupted input could cause damage

In those cases, let ValueError bubble up and fail fast. The key is being intentional. If you’re handling user input, convert it to a friendly error. If you’re processing internal data, fix the logic rather than masking the error.

Monitoring and observability for ValueError

In production, ValueError can signal data quality issues or sudden changes in upstream systems. I use monitoring to detect spikes in ValueError counts.

  • Log structured errors with field names and values
  • Add metrics for validation failure rates
  • Alert on unusual increases

This turns ValueError from “random bug” into a signal that something upstream changed. That’s invaluable in distributed systems.

A deeper checklist for production readiness

Before I ship a fix involving ValueError, I confirm:

  • Inputs are validated at the boundary
  • Validation functions are unit tested
  • Error messages are explicit and helpful
  • Invalid inputs are logged with repr()
  • There is a clear decision about dropping vs repairing bad data
  • API responses map ValueError to 4xx errors, not 5xx

It’s a short list, but it prevents the most common regressions.

Summary of how I actually fix ValueError

Here’s the distilled strategy I use in real projects:

  • Identify whether the error is at the boundary or internal logic
  • Print or log the raw value using repr()
  • Validate early and explicitly; don’t over‑sanitize
  • Use small, focused try/except blocks
  • Return clear error messages with expected formats
  • Add tests for common invalid cases

If you take only one idea away from this guide, make it this: ValueError isn’t a random exception. It’s a contract violation. Fix the contract, and the error goes away.

A final quick reference

  • Wrong type? It’s likely a TypeError.
  • Right type, wrong value? ValueError.
  • External input? Validate at the boundary.
  • Internal logic? Fix the logic, don’t catch and hide.

ValueError can feel annoying, but it’s also Python’s way of telling you your assumptions need tightening. Once you adopt the habit of validating early and reporting errors clearly, these exceptions become predictable, fast to fix, and far less likely to reach production.

Scroll to Top