Repository: samuelcolvin/pydantic
Pull request #3018 (2021-07-24 17:11:19)

Fix support for using a subclass of an annotation as a default
## Change Summary

A field can be set with a default that is a subclass of the type hint when set together:

```python
class MyStr(str):
    pass

class Simple(BaseModel):
    x: str = MyStr("test")
```

Additionally, a model can be instantiated with a subclass of the type hint: `Simple(x=MyStr("works"))`.

However, when using model subclasses where the base sets a type hint, but the default (eg: `MyStr`) is set in a
subclass, the `The type of {name}.{var_name} differs from the new default value` exception is raised:

```python
class MyStr(str):
    pass

class Base(BaseModel):
    x: str

class Sub(Base):
    x = MyStr("test")
```

I think rather than checking for an exact match on the type, we should be able to use `lenient_issubclass`. This seems
pretty sane and didn't break any of the existing tests. My use case is actually using an `Annotation(BaseModel)` as the
type hint, but setting subclass defaults like `Vendor(Annotation)`, but that should follow from this simpler test case.

## Related issue number

<!-- Are there any issues opened that will be resolved by merging this change? -->

## Checklist

* [x] Unit tests for the changes exist
* [x] Tests pass on CI and coverage remains at 100%
* [x] Documentation reflects the changes where applicable
* [x] `changes/<pull request or issue id>-<github username>.md` file added describing change
  (see [changes/README.md](https://github.com/samuelcolvin/pydantic/blob/master/changes/README.md) for details)
* [x] My PR is ready to review, **please add a comment including the phrase "please review" to assign reviewers**


JacobHayes: Please review when you have a moment. The change is a bit simpler ignoring whitespace changes.

Patch:
diff --git a/changes/3018-JacobHayes.md b/changes/3018-JacobHayes.md
new file mode 100644
index 000000000..3eb6ae9fe
--- /dev/null
+++ b/changes/3018-JacobHayes.md
@@ -0,0 +1,1 @@
+Fix support for using a subclass of an annotation as a default
diff --git a/pydantic/main.py b/pydantic/main.py
index 18848bc9f..7e1e3cbd4 100644
--- a/pydantic/main.py
+++ b/pydantic/main.py
@@ -217,11 +217,14 @@ class ModelMetaclass(ABCMeta):
                         class_validators=vg.get_validators(var_name),
                         config=config,
                     )
-                    if var_name in fields and inferred.type_ != fields[var_name].type_:
-                        raise TypeError(
-                            f'The type of {name}.{var_name} differs from the new default value; '
-                            f'if you wish to change the type of this field, please use a type annotation'
-                        )
+                    if var_name in fields:
+                        if lenient_issubclass(inferred.type_, fields[var_name].type_):
+                            inferred.type_ = fields[var_name].type_
+                        else:
+                            raise TypeError(
+                                f'The type of {name}.{var_name} differs from the new default value; '
+                                f'if you wish to change the type of this field, please use a type annotation'
+                            )
                     fields[var_name] = inferred
 
         _custom_root_type = ROOT_KEY in fields


Test patch:
diff --git a/tests/test_edge_cases.py b/tests/test_edge_cases.py
index aaef35487..c5bb67a0b 100644
--- a/tests/test_edge_cases.py
+++ b/tests/test_edge_cases.py
@@ -769,6 +769,27 @@ def test_inheritance():
     assert Bar().dict() == {'x': 12.3, 'a': 123.0}
 
 
+def test_inheritance_subclass_default():
+    class MyStr(str):
+        pass
+
+    # Confirm hint supports a subclass default
+    class Simple(BaseModel):
+        x: str = MyStr('test')
+
+    # Confirm hint on a base can be overridden with a subclass default on a subclass
+    class Base(BaseModel):
+        x: str
+        y: str
+
+    class Sub(Base):
+        x = MyStr('test')
+        y: MyStr = MyStr('test')  # force subtype
+
+    assert Sub.__fields__['x'].type_ == str
+    assert Sub.__fields__['y'].type_ == MyStr
+
+
 def test_invalid_type():
     with pytest.raises(RuntimeError) as exc_info: