Pydantic `Field(ge/gt/le/lt=N)` with int literal flagged as `bad-argument-type` on `Decimal` fields
- 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
Assessment
This issue has not been assessed yet.