Skip to content

Float Literals

# Decimal floats (64-bit)
pi = 3.14159
half = 0.5
negative = -2.718

# Leading decimal point is allowed (like Python and C#)
valid1 = 0.5
valid2 = 5.0
valid3 = .5    # Same as 0.5

# ❌ Invalid - trailing decimal point without digit
# invalid = 5.   # ERROR: use 5.0 instead

# Underscores for readability (optional)
precise = 3.141_592_653

Type Inference

  • Float literals with decimal point are inferred as float64 (System.Double)
  • Suffix notation for explicit typing (optional):
  • f or F for float32 (System.Single): 3.14f
  • d or D for float64 (System.Double): 3.14d (redundant but allowed)
  • m or M for decimal (System.Decimal): 3.14m

A suffix means the same on every spelling of a float — .5f is 0.5f, and 1e5f and 1_0.2_5m take it too. The integer suffixes L, u and ul (see Integer Literals) belong to integers; on a float literal they are refused (SPY0023), as in C#:

# x = 3.14L                 # ERROR (SPY0023): Invalid numeric suffix: L (a float literal accepts only the float suffixes f, d and m)
# y = 1e5u                  # ERROR (SPY0023): Invalid numeric suffix: u (a float literal accepts only the float suffixes f, d and m)

Narrowing an Unsuffixed Literal to float32 or decimal

An unsuffixed float literal may narrow to float32 or decimal at every store position — the same rule as integer constants (§10.2.11 above). The literal is re-typed and emitted with the appropriate suffix (f for float32, m for decimal):

from System.Numerics import Vector2

class Holder:
    ratio: float32 = 0.5       # field declaration

def ret() -> float32:
    return 0.1                  # return

def takes(x: float32 = 0.1) -> None:   # parameter default
    print(x)

def main() -> None:
    x: float32 = 0.1           # declaration
    x = 0.25                    # plain store
    print(x)                    # 0.25
    takes(0.5)                  # argument
    print(ret())                # 0.1
    xs: list[float32] = [0.5]   # collection-literal element
    v: Vector2 = Vector2(1.0, 2.0)  # CLR argument
    print(v.X)                  # 1.0

The same rule applies to decimal, at the same positions:

def money() -> decimal:
    return 0.1                  # return

def taxed(rate: decimal = 0.2) -> None:   # parameter default
    print(rate)

def main() -> None:
    d: decimal = 1.5            # declaration
    print(d)                    # 1.5
    e: decimal = 1.5e2          # exponent form narrows too
    print(e)                    # 150
    taxed(0.25)                 # argument                  -> 0.25
    print(money())              # 0.1
    ds: list[decimal] = [0.5]   # collection-literal element
    print(ds[0])                # 0.5

Two limits:

  • Only a literal narrows. A float64-typed expression or variable still requires an explicit cast: x: float32 = y is SPY0220 for y: float.
  • The value must be within float32's range. x: float32 = 1e40 is SPY0220, not a silent infinity. The same holds for decimal, whose range is about ±7.9e28: d: decimal = 1e29 is SPY0220, and so does a non-zero literal below decimal's smallest non-zero value: d: decimal = 1e-30 is SPY0220.

This is a deliberate divergence from C#, which rejects float f = 0.1; outright. Precision loss within range is accepted — writing the annotation is taken as asking for it.

A Literal Must Fit Its Own Type

Whatever its spelling, a float literal must have a finite value in the type it resolves to — float64 unsuffixed, float32 with f, decimal with m. A literal outside that range is refused at compile time with SPY0210, the code an out-of-range integer literal draws, and never carried as an infinity. Python reads 1e400 as inf; Sharpy refuses it, because a silent infinity would flow on through arithmetic.

def main() -> None:
    big = 1.7976931348623157e308          # float64's largest value
    print(big)                            # 1.7976931348623157e+308
    f = 3.4028235e38f                     # float32's largest value
    print(f)                              # 3.4028235e+38
    m = 79228162514264337593543950335m    # decimal's largest value
    print(m)                              # 79228162514264337593543950335
    # x = 1e400                           # ERROR (SPY0210): Float literal '1e400' is too large for 'double'
    # y = 1e39f                           # ERROR (SPY0210): Float literal '1e39' is too large for 'float'
    # z = 1e29m                           # ERROR (SPY0210): Float literal '1e29' is too large for 'decimal'
    tiny = 1e-28m                         # decimal's smallest non-zero value
    print(tiny)                           # 0.0000000000000000000000000001
    print(1e-400)                         # 0.0
    # w = 1e-30m                          # ERROR (SPY0210): Float literal '1e-30' is too small for 'decimal': its value would be zero

A negated literal is checked by its digits, so -1e400 is refused the same way. Below the smallest non-zero value the types part ways. A float32 or float64 literal too small to represent is IEEE zero, not out of range: 1e-400 is 0.0, as in Python. A non-zero decimal literal whose value would be zero (1e-30m, 1.5e-29m; decimal's smallest non-zero magnitude is 1e-28) is refused with SPY0210, on the suffixed and the slot-narrowed spelling alike, because a decimal is chosen for exact values and a silent zero is the wrong one. A literal with more than decimal's 28–29 significant digits is rounded, not refused (the decimal-literal-28-digit-precision deviation).

Implementation - ✅ Native - Direct mapping to C# float literals with compiler-inserted suffixes.