Repository: pydantic/pydantic

Issue:
Const validation applied before type validation/parsing
# Question

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.5
            pydantic compiled: False
                 install path: D:\dev\github\pydantic\issues\.venv\Lib\site-packages\pydantic
               python version: 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 18 2019, 23:46:00) [MSC v.1916 32 bit (Intel)]
                     platform: Windows-10-10.0.19559-SP0
     optional deps. installed: []
```

```py
import os
from pathlib import Path
import pydantic

class Config1(pydantic.BaseSettings):
    port: int = pydantic.Field(1234, const=True)

class Config2(pydantic.BaseSettings):
    port: int = pydantic.Field("1234", const=True)

print("No env")
print("Config1", Config1().dict())
print("Config2", Config2().dict())

print("With .env")
Path(".env").write_text("port=1234\n")
print("Config1", Config1(_env_file=".env").dict())  # FAILS
print("Config2", Config2(_env_file=".env").dict())

print("With os.environ")
os.environ["port"] = "1234"
print("Config1", Config1().dict())  # FAILS
print("Config2", Config2().dict())
```
Copy-paste in the Python REPL to continue on errors:
```python
>>> import os
>>> from pathlib import Path
>>> import pydantic
>>> 
>>> class Config1(pydantic.BaseSettings):
...     port: int = pydantic.Field(1234, const=True)  
...
>>> class Config2(pydantic.BaseSettings):
...     port: int = pydantic.Field("1234", const=True)
...
>>> print("No env")
No env
>>> print("Config1", Config1().dict())
Config1 {'port': 1234}
>>> print("Config2", Config2().dict())
Config2 {'port': 1234}
>>>
>>> print("With .env")
With .env
>>> Path(".env").write_text("port=1234\n")
10
>>> print("Config1", Config1(_env_file=".env").dict())  # FAILS
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\env_settings.py", line 28, in __init__
    super().__init__(**__pydantic_self__._build_values(values, _env_file=_env_file))
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\main.py", line 338, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Config1
port
  unexpected value; permitted: 1234 (type=value_error.const; given=1234; permitted=[1234])
>>> print("Config2", Config2(_env_file=".env").dict())
Config2 {'port': 1234}
>>>
>>> print("With os.environ")
With os.environ
>>> os.environ["port"] = "1234"
>>> print("Config1", Config1().dict())  # FAILS
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\env_settings.py", line 28, in __init__
    super().__init__(**__pydantic_self__._build_values(values, _env_file=_env_file))
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\main.py", line 338, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Config1
port
  unexpected value; permitted: 1234 (type=value_error.const; given=1234; permitted=[1234])
>>> print("Config2", Config2().dict())
Config2 {'port': 1234}
```

Looked at the code, and noticed that `constant_validator` was applied with `pre_validators` -- before the type validators -- which explains the behaviour.

My question is: is this by design? It looks odd to have the default value be of a different type than the annotation. Additionally, if the value comes from a JSON object instead, then using `const` means the input type is now constrained to the type of the default value. For instance:
```python
>>> import pydantic
>>>
>>> class Config1(pydantic.BaseSettings):
...     port_const: int = pydantic.Field(1234, const=True)
...     port: int = pydantic.Field(1234)
...
>>> print(Config1())
port_const=1234 port=1234
>>> print(Config1(port_const=1234, port=1234))
port_const=1234 port=1234
>>> print(Config1(port_const="1234", port="1234"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\env_settings.py", line 28, in __init__
    super().__init__(**__pydantic_self__._build_values(values, _env_file=_env_file))
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\main.py", line 338, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Config1
port_const
  unexpected value; permitted: 1234 (type=value_error.const; given=1234; permitted=[1234])
```
Const validation applied before type validation/parsing
# Question

Output of `python -c "import pydantic.utils; print(pydantic.utils.version_info())"`:
```
             pydantic version: 1.5
            pydantic compiled: False
                 install path: D:\dev\github\pydantic\issues\.venv\Lib\site-packages\pydantic
               python version: 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 18 2019, 23:46:00) [MSC v.1916 32 bit (Intel)]
                     platform: Windows-10-10.0.19559-SP0
     optional deps. installed: []
```

```py
import os
from pathlib import Path
import pydantic

class Config1(pydantic.BaseSettings):
    port: int = pydantic.Field(1234, const=True)

class Config2(pydantic.BaseSettings):
    port: int = pydantic.Field("1234", const=True)

print("No env")
print("Config1", Config1().dict())
print("Config2", Config2().dict())

print("With .env")
Path(".env").write_text("port=1234\n")
print("Config1", Config1(_env_file=".env").dict())  # FAILS
print("Config2", Config2(_env_file=".env").dict())

print("With os.environ")
os.environ["port"] = "1234"
print("Config1", Config1().dict())  # FAILS
print("Config2", Config2().dict())
```
Copy-paste in the Python REPL to continue on errors:
```python
>>> import os
>>> from pathlib import Path
>>> import pydantic
>>> 
>>> class Config1(pydantic.BaseSettings):
...     port: int = pydantic.Field(1234, const=True)  
...
>>> class Config2(pydantic.BaseSettings):
...     port: int = pydantic.Field("1234", const=True)
...
>>> print("No env")
No env
>>> print("Config1", Config1().dict())
Config1 {'port': 1234}
>>> print("Config2", Config2().dict())
Config2 {'port': 1234}
>>>
>>> print("With .env")
With .env
>>> Path(".env").write_text("port=1234\n")
10
>>> print("Config1", Config1(_env_file=".env").dict())  # FAILS
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\env_settings.py", line 28, in __init__
    super().__init__(**__pydantic_self__._build_values(values, _env_file=_env_file))
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\main.py", line 338, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Config1
port
  unexpected value; permitted: 1234 (type=value_error.const; given=1234; permitted=[1234])
>>> print("Config2", Config2(_env_file=".env").dict())
Config2 {'port': 1234}
>>>
>>> print("With os.environ")
With os.environ
>>> os.environ["port"] = "1234"
>>> print("Config1", Config1().dict())  # FAILS
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\env_settings.py", line 28, in __init__
    super().__init__(**__pydantic_self__._build_values(values, _env_file=_env_file))
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\main.py", line 338, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Config1
port
  unexpected value; permitted: 1234 (type=value_error.const; given=1234; permitted=[1234])
>>> print("Config2", Config2().dict())
Config2 {'port': 1234}
```

Looked at the code, and noticed that `constant_validator` was applied with `pre_validators` -- before the type validators -- which explains the behaviour.

My question is: is this by design? It looks odd to have the default value be of a different type than the annotation. Additionally, if the value comes from a JSON object instead, then using `const` means the input type is now constrained to the type of the default value. For instance:
```python
>>> import pydantic
>>>
>>> class Config1(pydantic.BaseSettings):
...     port_const: int = pydantic.Field(1234, const=True)
...     port: int = pydantic.Field(1234)
...
>>> print(Config1())
port_const=1234 port=1234
>>> print(Config1(port_const=1234, port=1234))
port_const=1234 port=1234
>>> print(Config1(port_const="1234", port="1234"))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\env_settings.py", line 28, in __init__
    super().__init__(**__pydantic_self__._build_values(values, _env_file=_env_file))
  File "D:\dev\github\pydantic\issues\.venv\lib\site-packages\pydantic\main.py", line 338, in __init__
    raise validation_error
pydantic.error_wrappers.ValidationError: 1 validation error for Config1
port_const
  unexpected value; permitted: 1234 (type=value_error.const; given=1234; permitted=[1234])
```


Discussion:
I agree this is wrong.

I can't remember exactly why the const validator is applied as a pre-validator.

If you'd be interested in working on this, feel free to try moving it to a post-validator and see if any tests fail - that might tell you why it had to be a pre-validator.

If we can fix this without backwards incompatible changes, it can go out soon, otherwise it might have to wait until v2.
Cool, I'll give it a shot!

Also just came across this:

```python
import pydantic
import os

class Config1(pydantic.BaseSettings):
    port: int = pydantic.Field("1234", const=True)

class Config2(pydantic.BaseModel):
    port: int = pydantic.Field("1234", const=True)


print(Config1())  # 1234
print(Config2())  # '1234'
```
Pretty sure it's the same problem.
That's different, it's because `BaseSettings` has the [`validate_all`](https://pydantic-docs.helpmanual.io/usage/model_config/) config setting set to `True` by default.
I agree this is wrong.

I can't remember exactly why the const validator is applied as a pre-validator.

If you'd be interested in working on this, feel free to try moving it to a post-validator and see if any tests fail - that might tell you why it had to be a pre-validator.

If we can fix this without backwards incompatible changes, it can go out soon, otherwise it might have to wait until v2.
Cool, I'll give it a shot!

Also just came across this:

```python
import pydantic
import os

class Config1(pydantic.BaseSettings):
    port: int = pydantic.Field("1234", const=True)

class Config2(pydantic.BaseModel):
    port: int = pydantic.Field("1234", const=True)


print(Config1())  # 1234
print(Config2())  # '1234'
```
Pretty sure it's the same problem.
That's different, it's because `BaseSettings` has the [`validate_all`](https://pydantic-docs.helpmanual.io/usage/model_config/) config setting set to `True` by default.

Resolving patch:
diff --git a/pydantic/fields.py b/pydantic/fields.py
--- a/pydantic/fields.py
+++ b/pydantic/fields.py
@@ -510,11 +510,11 @@ def populate_validators(self) -> None:
             )
             self.validators = prep_validators(v_funcs)
 
-        # Add const validator
         self.pre_validators = []
         self.post_validators = []
+
         if self.field_info and self.field_info.const:
-            self.pre_validators = [make_generic_validator(constant_validator)]
+            self.post_validators.append(make_generic_validator(constant_validator))
 
         if class_validators_:
             self.pre_validators += prep_validators(v.func for v in class_validators_ if not v.each_item and v.pre)


Test patch:
diff --git a/tests/test_main.py b/tests/test_main.py
--- a/tests/test_main.py
+++ b/tests/test_main.py
@@ -383,6 +383,15 @@ class Model(BaseModel):
     assert m.a == 3
 
 
+def test_const_validates_after_type_validators():
+    # issue #1410
+    class Model(BaseModel):
+        a: int = Field(3, const=True)
+
+    m = Model(a='3')
+    assert m.a == 3
+
+
 def test_const_with_wrong_value():
     class Model(BaseModel):
         a: int = Field(3, const=True)