facebook / facebook/pyrefly

Pydantic `Field(ge/gt/le/lt=N)` with int literal flagged as `bad-argument-type` on `Decimal` fields

Open
#3,403 3 comments 3 reactions 1 assignee Claimed by @asukaminato0721 View on GitHub
pydantic typechecking
Dominant language
Rust
Stars
7k
Forks
516
PR merge metrics
No merged PRs in 30d

Description

### Describe the Bug

Pyrefly's automatic Pydantic support handles `Field(ge=0)` / `Field(gt=0)` etc. correctly when the field type is `int` or `float`, but emits a false-positive `bad-argument-type` error when the same constraint is applied to a `Decimal` (or `Decimal | None`) field.

This appears related to the protocol-subset-check issue cited by @migeed-z in [#974](https://github.com/facebook/pyrefly/issues/974#issuecomment-3276729649) ("this error has to do with the subset check for protocols not working correctly"). Issue #974 was closed as fixed for `int` fields (confirmed working in this repro), but the fix did not extend to `Decimal`.

### Repro

```python
from decimal import Decimal
from pydantic import BaseModel, Field

# Baseline (clean — works correctly):
class IntField(BaseModel):
a: int = Field(ge=0)
b: int = Field(gt=0)
c: int = Field(le=100)
d: int = Field(lt=100)

class FloatField(BaseModel):
a: float = Field(ge=0)
b: float = Field(gt=0)
c: float = Field(le=100)
d: float = Field(lt=100)

# Bug — all four constraints flagged on Decimal fields:
class DecimalField(BaseModel):
a: Decimal = Field(ge=0)
b: Decimal = Field(gt=0)
c: Decimal = Field(le=100)
d: Decimal = Field(lt=100)

class DecimalOptionalField(BaseModel):
a: Decimal | None = Field(default=None, ge=0)

# Workaround that does NOT trigger the error:
class DecimalExplicit(BaseModel):
a: Decimal = Field(ge=Decimal("0"))
b: Decimal = Field(gt=Decimal("0"))
```

### Actual Output

```
ERROR Pydantic `ge` value has type `Literal[0]`, which is not assignable to field type `Decimal` [bad-argument-type]
ERROR Pydantic `gt` value has type `Literal[0]`, which is not assignable to field type `Decimal` [bad-argument-type]
ERROR Pydantic `le` value has type `Literal[100]`, which is not assignable to field type `Decimal` [bad-argument-type]
ERROR Pydantic `lt` value has type `Literal[100]`, which is not assignable to field type `Decimal` [bad-argument-type]
ERROR Pydantic `ge` value has type `Literal[0]`, which is not assignable to field type `Decimal | None` [bad-argument-type]
INFO 5 errors
```

### Expected Behavior

No errors. Two reasons:

1. **Runtime**: Pydantic coerces `int` → `Decimal` automatically. `Field(ge=0)` on a `Decimal` field is valid Python that works at runtime.
2. **Other type checkers**: `mypy --strict` with the `pydantic.mypy` plugin reports the same code as clean (`Success: no issues found`).

Pydantic's own [numeric-constraints docs](https://docs.pydantic.dev/latest/concepts/fields/#numeric-constraints) demonstrate `Field(gt=0)` patterns without typing the constraint value to match the field type; this is intentional API ergonomics.

### Workaround

Wrapping the constraint value explicitly with `Decimal(...)` suppresses the error:

```python
a: Decimal = Field(ge=Decimal("0")) # clean
```

But this defeats the readability/ergonomics the Pydantic API is designed for, and would require touching every constraint declaration on every `Decimal` field across a codebase.

### Why This Matters

Financial / quantitative / scientific Python codebases use `Decimal` extensively for money, prices, sizes, ratios, and any value where binary-float drift is unacceptable. `Field(ge=0)` on a `Decimal` price is the single most common constraint pattern in those domains.

In a real codebase audit, this single rule generated 30+ false positives from a `pyrefly check` run on ~50k LOC, blocking adoption despite Pyrefly being otherwise faster than `mypy`. Fixing this would unblock Pyrefly adoption for an entire class of Pydantic-heavy users.

### Environment

- Pyrefly: `1.0.0`
- Pydantic: `2.13.4`
- Python: `3.14.0`
- Config: no `pyrefly.toml` (default `legacy` preset reading `[tool.mypy]` from `pyproject.toml`)
- Comparison checker: `mypy 1.x` with `plugins = ["pydantic.mypy"]` → clean on the same code.

### Related

- #974 — closed; fixed `int` field constraints; the same root cause (protocol-subset check) appears to be unfixed for `Decimal`.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.