It's probably a consequence of the parsing algorithm used. A simple mental model is that the tokenizer attempts to match all the token patterns there are, and recognizes the longest match it finds. On a lower-level, the tokenizer works character-by-character, and makes a decision based only on the current state and input character – there shouldn't be any backtracking or re-reading of input.

After joining patterns with common prefixes – in this case, the pattern for int literals and the integral part of the pattern of float literals – what happens in the tokenizer is that it:

  1. Reads the 1, and enters the state that indicates "reading either a float or an int literal"
  2. Reads the ., and enters the state "reading a float literal"
  3. Reads the _, which can not be part of a float literal. The parser emits 1. as a float literal token.
  4. Carries on parsing starting with the _, and eventually emits __class__ as an identifier token.

Aside: This tokenizing approach is also the reason why common languages have the syntax restrictions they have. E.g. identifiers contain letters, digits, and underscores, but cannot start with a digit. If that was allowed, 123abc could be intended as either an identifier, or the integer 123 followed by the identifier abc.

A lex-like tokenizer would recognize this as the former since it leads to the longest single token, but nobody likes having to keep details like this in their head when trying to read code. Or when trying to write and debug the tokenizer for that matter.


The parser then tries to process the token stream:

<FloatLiteral: '1.'> <Identifier: '__class__'>

In Python, a literal directly followed by an identifier – without an operator between the tokens – makes no sense, so the parser bails. This also means that the reason why Python would complain about 123abc being invalid syntax isn't the tokenizer error "the character a isn't valid in an integer literal", but the parser error "the identifier abc cannot directly follow the integer literal 123"


The reason why the tokenizer can't recognize the 1 as an int literal is that the character that makes it leave the float-or-int state determines what it just read. If it's ., it was the start of a float literal, which might continue afterwards. If it's something else, it was a complete int literal token.

It's not possible for the tokenizer to "go back" and re-read the previous input as something else. In fact, the tokenizer is at too low a level to care about what an "attribute access" is and handle such ambiguities.


Now, your second example is valid because the tokenizer knows a float literal can only have one . in it. More precisely: the first . makes it transition from the float-or-int state to the float state. In this state, it only expects digits (or an E for scientific/engineering notation, a j for complex numbers…) to continue the the float literal. The first character that's not a digit etc. (i.e. the .) is definitely no longer part of the float literal and the tokenizer can emit the finished token. The token stream for your second example will thus be:

<FloatLiteral: '1.'> <Operator: '.'> <Identifier: '__class__'>

Which, of course, the parser then recognizes as valid Python. Now we also know enough why the suggested workarounds help. In Python, separating tokens with whitespace is optional – unlike, say, in Lisp. Conversely, whitespace does separate tokens. (That is, no tokens except string literals may contain whitespace, it's merely skipped between tokens.) So the code:

1 .__class__

is always tokenized as

<IntLiteral: '1'> <Operator: '.'> <Identifier: '__class__'>

And since a closing parenthesis cannot appear in an int literal, this:

(1).__class__

gets read as this:

<Operator: '('> <IntLiteral: '1'> <Operator: ')'> <Operator: '.'> <Identifier: '__class__'>

The above implies that, amusingly, the following is also valid:

1..__class__ # => <type 'float'>

The decimal part of a float literal is optional, and the second . read will make the preceding input be recognized as one.

Answer from millimoose on Stack Overflow
🌐
Python
docs.python.org › 3 › c-api › float.html
Floating-Point Objects — Python 3.14.4 documentation
The pack and unpack functions provide an efficient platform-independent way to store floating-point values as byte strings. The Pack routines produce a bytes string from a C double, and the Unpack routines produce a C double from such a bytes string.
Top answer
1 of 5
31

It's probably a consequence of the parsing algorithm used. A simple mental model is that the tokenizer attempts to match all the token patterns there are, and recognizes the longest match it finds. On a lower-level, the tokenizer works character-by-character, and makes a decision based only on the current state and input character – there shouldn't be any backtracking or re-reading of input.

After joining patterns with common prefixes – in this case, the pattern for int literals and the integral part of the pattern of float literals – what happens in the tokenizer is that it:

  1. Reads the 1, and enters the state that indicates "reading either a float or an int literal"
  2. Reads the ., and enters the state "reading a float literal"
  3. Reads the _, which can not be part of a float literal. The parser emits 1. as a float literal token.
  4. Carries on parsing starting with the _, and eventually emits __class__ as an identifier token.

Aside: This tokenizing approach is also the reason why common languages have the syntax restrictions they have. E.g. identifiers contain letters, digits, and underscores, but cannot start with a digit. If that was allowed, 123abc could be intended as either an identifier, or the integer 123 followed by the identifier abc.

A lex-like tokenizer would recognize this as the former since it leads to the longest single token, but nobody likes having to keep details like this in their head when trying to read code. Or when trying to write and debug the tokenizer for that matter.


The parser then tries to process the token stream:

<FloatLiteral: '1.'> <Identifier: '__class__'>

In Python, a literal directly followed by an identifier – without an operator between the tokens – makes no sense, so the parser bails. This also means that the reason why Python would complain about 123abc being invalid syntax isn't the tokenizer error "the character a isn't valid in an integer literal", but the parser error "the identifier abc cannot directly follow the integer literal 123"


The reason why the tokenizer can't recognize the 1 as an int literal is that the character that makes it leave the float-or-int state determines what it just read. If it's ., it was the start of a float literal, which might continue afterwards. If it's something else, it was a complete int literal token.

It's not possible for the tokenizer to "go back" and re-read the previous input as something else. In fact, the tokenizer is at too low a level to care about what an "attribute access" is and handle such ambiguities.


Now, your second example is valid because the tokenizer knows a float literal can only have one . in it. More precisely: the first . makes it transition from the float-or-int state to the float state. In this state, it only expects digits (or an E for scientific/engineering notation, a j for complex numbers…) to continue the the float literal. The first character that's not a digit etc. (i.e. the .) is definitely no longer part of the float literal and the tokenizer can emit the finished token. The token stream for your second example will thus be:

<FloatLiteral: '1.'> <Operator: '.'> <Identifier: '__class__'>

Which, of course, the parser then recognizes as valid Python. Now we also know enough why the suggested workarounds help. In Python, separating tokens with whitespace is optional – unlike, say, in Lisp. Conversely, whitespace does separate tokens. (That is, no tokens except string literals may contain whitespace, it's merely skipped between tokens.) So the code:

1 .__class__

is always tokenized as

<IntLiteral: '1'> <Operator: '.'> <Identifier: '__class__'>

And since a closing parenthesis cannot appear in an int literal, this:

(1).__class__

gets read as this:

<Operator: '('> <IntLiteral: '1'> <Operator: ')'> <Operator: '.'> <Identifier: '__class__'>

The above implies that, amusingly, the following is also valid:

1..__class__ # => <type 'float'>

The decimal part of a float literal is optional, and the second . read will make the preceding input be recognized as one.

2 of 5
27

It is a tokenization issue... the . is parsed as the beginning of the fractional part of a floating point number.

You can use

(1).__class__

to avoid the problem

🌐
GitHub
github.com › python › cpython › blob › main › Objects › floatobject.c
cpython/Objects/floatobject.c at main · python/cpython
Convert real number to a floating-point number. ... You probably don't want to use this function. ... It exists mainly to be used in Python's test suite.
Author   python
🌐
Python documentation
docs.python.org › 3 › library › functions.html
Built-in Functions — Python 3.14.4 documentation
February 27, 2026 - For example, metaclass attributes are not in the result list when the argument is a class. ... Take two (non-complex) numbers as arguments and return a pair of numbers consisting of their quotient and remainder when using integer division. With mixed operand types, the rules for binary arithmetic operators apply. For integers, the result is the same as (a // b, a % b). For floating-point numbers the result is (q, a % b), where q is usually math.floor(a / b) but may be 1 less than that.
🌐
Python documentation
docs.python.org › 3 › library › functions.html
Built-in Functions — Python 3.13.5 documentation
For example, metaclass attributes are not in the result list when the argument is a class. ... Take two (non-complex) numbers as arguments and return a pair of numbers consisting of their quotient and remainder when using integer division. With mixed operand types, the rules for binary arithmetic operators apply. For integers, the result is the same as (a // b, a % b). For floating-point numbers the result is (q, a % b), where q is usually math.floor(a / b) but may be 1 less than that.
🌐
AskPython
askpython.com › home › the python float() method
The Python float() Method - AskPython
August 6, 2022 - Basically, the Python float() function is used for converting some data from other types like integer, string or etc., to the type float. It is also used for declaring floating-point type variables.
🌐
W3Schools
w3schools.com › python › ref_func_float.asp
Python float() Function
Python Examples Python Compiler ... Q&A Python Bootcamp Python Training ... The float() function converts the specified value into a floating point number....
Find elsewhere
🌐
Programiz
programiz.com › python-programming › methods › built-in › float
Python float()
x (Optional) - number or string that needs to be converted to floating point number If it's a string, the string should contain decimal points
🌐
Python documentation
docs.python.org › 3 › library › stdtypes.html
Built-in Types — Python 3.14.4 documentation
For ease of implementation and efficiency across a variety of numeric types (including int, float, decimal.Decimal and fractions.Fraction) Python’s hash for numeric types is based on a single mathematical function that’s defined for any rational number, and hence applies to all instances of int and fractions.Fraction, and all finite instances of float and decimal.Decimal. Essentially, this function is given by reduction modulo P for a fixed prime P. The value of P is made available to Python as the modulus attribute of sys.hash_info.
🌐
Blender
docs.blender.org › api › current › bpy.types.FloatAttribute.html
FloatAttribute(Attribute) - Blender Python API
base classes — bpy_struct, Attribute · class bpy.types.FloatAttribute(Attribute)¶ · Geometry attribute that stores floating-point values · data¶ · Type: bpy_prop_collection of FloatAttributeValue, (readonly) classmethod bl_rna_get_subclass(id, default=None, /)¶ ·
🌐
GeeksforGeeks
geeksforgeeks.org › float-in-python
float() in Python - GeeksforGeeks
May 10, 2023 - If an argument is passed, then the equivalent floating-point number is returned. If no argument is passed then the method returns 0.0. If any string is passed that is not a decimal point number or does not match any cases mentioned above then an error will be raised. If a number is passed outside the range of Python float then OverflowError is generated.
🌐
Bobby Hadz
bobbyhadz.com › blog › python-attributeerror-float-object-has-no-attribute
AttributeError: 'float' object has no attribute 'X' (Python) | bobbyhadz
Copied!example = 3.6 # ⛔️ AttributeError: 'float' object has no attribute 'round' result = example.round()
🌐
Real Python
realpython.com › python-data-types
Basic Data Types in Python: A Quick Exploration – Real Python
December 21, 2024 - In practice, the difference between the actual and represented values is small and should be manageable. However, check out Make Python Lie to You for some challenges you should be aware of. ... The built-in float type has a few methods and attributes which can be useful in some situations.
🌐
Heurekadevs
heurekadevs.com › homepage › home › development › on the implementation of float and list types in python: part i
On The Implementation of Float and List Types in Python: Part I | Heurekadevs
January 26, 2022 - Throughout the post I'll make a few simplifications (e.g. regarding any reusing of objects by the interpreter) to keep it brief. However, there should not be much loss of generality when we think of arbitrarily large lists and random floats. I describe these structures as they are implemented in CPython (specifically CPython 3.10.0). Let us start by saying that all data types in Python are objects.
Top answer
1 of 1
7

Mutable objects always create a new object, otherwise the data would be shared. There's not much to explain here, as if you append an item to an empty list, you don't want all of the empty lists to have that item.

Immutable objects behave in a completely different manner:

  • Strings get interned. If they are smaller than 20 alphanumeric characters, and are static (consts in the code, function names, etc), they get cached and are accessed from a special mapping reserved for these. It is to save memory but more importantly used to have a faster comparison. Python uses a lot of dictionary access operations under the hood which require string comparison. Being able to compare 2 strings like attribute or function names by comparing their memory address instead of the actual value, is a significant runtime improvement.

  • Booleans simply return the same object. Considering there are only 2 available, it makes no sense creating them again and again.

  • Small integers (from -5 to 256) by default, are also cached. These are used quite often, just about everywhere. Every time an integer is in that range, CPython simply returns the same object.

Floats however are not cached. Unlike integers, where the numbers 0-10 are extremely common, 1.0 isn't guaranteed to be more used than 2.0 or 0.1. That's why float() simply returns a new float. We could have optimized the empty float(), and we can check for speed benefits but it might not have made such a difference.

The confusion starts to arise when float(0.0) is float(0.0). Python has numerous optimizations built in:

  • First of all, consts are saved in each function's code object. 0.0 is 0.0 simply refers to the same object. It is a compile-time optimization.

  • Second of all, float(0.0) takes the 0.0 object, and since it's a float (which is immutable), it simply returns it. No need to create a new object if it's already a float.

  • Lastly, 1.0 + 1.0 is 2.0 will also work. The reason is that 1.0 + 1.0 is calculated on compile time and then references the same 2.0 object:

    def test():
        return 1.0 + 1.0 is 2.0
    
    dis.dis(test)
      2           0 LOAD_CONST               1 (2.0)
                  2 LOAD_CONST               1 (2.0)
                  4 IS_OP                    0
                  6 RETURN_VALUE
    

    As you can see, there is no addition operation. The function was compiled with the result pointing to the exact same constant object.

So while there is no float-specific optimization, 3 different generic optimizations are into play. The sum of them is what ultimately decides if it'll be the same object or not.

🌐
Python
docs.python.org › 3.9 › c-api › float.html
Floating Point Objects — Python 3.9.24 documentation
Return a structseq instance which contains information about the precision, minimum and maximum values of a float.