Repository: brentyi/tyro (language: python)

Issue:
NewType support with Pydantic
I'm curious how NewTypes are handled underneath the hood and if there's support for it or if I have to modify the type hint. I have something like

`Microliter = NewType("Microliter", float)` which helps keep track of units and in a pydantic model it ends up resolving okay, but not in tyro

```
class Sample(BaseModel) 
     initial_volume: Microliter = pydantic.Field(50) 
```

Underneath, tyro hits a signature error with `typing.Microliter is not supported by signature` 

```
  File ".../tyro/_fields.py", line 448, in _try_field_list_from_callable
    return _try_field_list_from_general_callable(f, cls, default_instance)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File ".../tyro/_fields.py", line 852, in _try_field_list_from_general_callable
    params = list(inspect.signature(f).parameters.values())
```
tyro.conf.arg aliases not working in Pydantic>=2
```python
# tyrotest.py
from typing import Annotated

import tyro
from pydantic import BaseModel
from tyro.conf import arg

class AliasCfg(BaseModel):
    alias: Annotated[str, arg(aliases=["-a"])]

cfg = tyro.cli(AliasCfg)
print(f"Alias: {cfg.alias}")
```

With Pydantic < 2:
```console
$ python tyrotest.py
usage: tyrotest.py [-h] --alias STR

╭─ arguments ─────────────────────────────────────────────╮
│ -h, --help              show this help message and exit │
│ --alias STR, -a STR     (required)                      │
╰─────────────────────────────────────────────────────────╯
```

With Pydantic >=2:
```console
$ python tyrotest.py
usage: tyrotest.py [-h] --alias STR

╭─ arguments ────────────────────────────────────────╮
│ -h, --help         show this help message and exit │
│ --alias STR        (required)                      │
╰────────────────────────────────────────────────────╯
```
and of course the alias is not recognized on invocation.

With functions and dataclasses the alias works perfectly.


Discussion / hints:

Fix patch:
diff --git a/.flake8 b/.flake8
deleted file mode 100644
index a0bebef2..00000000
--- a/.flake8
+++ /dev/null
@@ -1,6 +0,0 @@
-[flake8]
-# E203: whitespace before :
-# E501: line too long (<n> characters)
-# W503: line break before binary operator
-; ignore = E203,E501,D100,D101,D102,D103,W503
-ignore = E203,E501,W503
diff --git a/examples/02_nesting/02_subcommands.py b/examples/02_nesting/02_subcommands.py
index 7948b2ab..6eb4d661 100644
--- a/examples/02_nesting/02_subcommands.py
+++ b/examples/02_nesting/02_subcommands.py
@@ -16,18 +16,21 @@
 from __future__ import annotations
 
 import dataclasses
-from typing import Union
+from typing import NewType, Union
 
 import tyro
 
 
 @dataclasses.dataclass(frozen=True)
-class Checkout:
+class Checkout_:
     """Checkout a branch."""
 
     branch: str
 
 
+Checkout = NewType("Checkout", Checkout_)
+
+
 @dataclasses.dataclass(frozen=True)
 class Commit:
     """Commit changes."""
diff --git a/pyproject.toml b/pyproject.toml
index 78585370..47e95e96 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -54,11 +54,6 @@ dev = [
 [project.urls]
 "GitHub" = "https://github.com/brentyi/tyro"
 
-[tool.setuptools.packages.find]
-exclude = [
-    "tests*"
-]
-
 [tool.setuptools.package-data]
 tyro = ["py.typed"]
 
diff --git a/tyro/__init__.py b/src/tyro/__init__.py
similarity index 100%
rename from tyro/__init__.py
rename to src/tyro/__init__.py
diff --git a/tyro/_argparse_formatter.py b/src/tyro/_argparse_formatter.py
similarity index 100%
rename from tyro/_argparse_formatter.py
rename to src/tyro/_argparse_formatter.py
diff --git a/tyro/_arguments.py b/src/tyro/_arguments.py
similarity index 100%
rename from tyro/_arguments.py
rename to src/tyro/_arguments.py
diff --git a/tyro/_calling.py b/src/tyro/_calling.py
similarity index 99%
rename from tyro/_calling.py
rename to src/tyro/_calling.py
index a12718f1..d1cfd6ee 100644
--- a/tyro/_calling.py
+++ b/src/tyro/_calling.py
@@ -220,7 +220,9 @@ def get_value_from_arg(prefixed_field_name: str) -> Any:
     # `tuple`.
     unwrapped_f = f
     unwrapped_f = _resolver.unwrap_origin_strip_extras(unwrapped_f)
-    unwrapped_f = _resolver.narrow_subtypes(unwrapped_f, default_instance)
+    unwrapped_f = _resolver.unwrap_newtype_and_narrow_subtypes(
+        unwrapped_f, default_instance
+    )
     unwrapped_f = _resolver.unwrap_origin_strip_extras(unwrapped_f)
     unwrapped_f = list if unwrapped_f is Sequence else unwrapped_f  # type: ignore
 
diff --git a/tyro/_cli.py b/src/tyro/_cli.py
similarity index 100%
rename from tyro/_cli.py
rename to src/tyro/_cli.py
diff --git a/tyro/_deprecated.py b/src/tyro/_deprecated.py
similarity index 100%
rename from tyro/_deprecated.py
rename to src/tyro/_deprecated.py
diff --git a/tyro/_docstrings.py b/src/tyro/_docstrings.py
similarity index 100%
rename from tyro/_docstrings.py
rename to src/tyro/_docstrings.py
diff --git a/tyro/_fields.py b/src/tyro/_fields.py
similarity index 98%
rename from tyro/_fields.py
rename to src/tyro/_fields.py
index 1ba9b168..3c9c05a4 100644
--- a/tyro/_fields.py
+++ b/src/tyro/_fields.py
@@ -1,4 +1,4 @@
-"""Abstractions for pulling out 'field' definitions, which specify inputs, types, and
+"""Abstractions for pulling out 'field' definitions, which specify inputs, types, and # type: ignore
 defaults, from general callables."""
 from __future__ import annotations
 
@@ -32,7 +32,14 @@
 
 import docstring_parser
 import typing_extensions
-from typing_extensions import NotRequired, Required, get_args, get_origin, is_typeddict
+from typing_extensions import (
+    Annotated,
+    NotRequired,
+    Required,
+    get_args,
+    get_origin,
+    is_typeddict,
+)
 
 from . import conf  # Avoid circular import.
 from . import (
@@ -262,7 +269,7 @@ def field_list_from_callable(
     """
     # Resolve generic types.
     f, type_from_typevar = _resolver.resolve_generic_types(f)
-    f = _resolver.narrow_subtypes(f, default_instance)
+    f = _resolver.unwrap_newtype_and_narrow_subtypes(f, default_instance)
 
     # Try to generate field list.
     field_list = _try_field_list_from_callable(f, default_instance)
@@ -372,7 +379,7 @@ def _try_field_list_from_callable(
     # Unwrap generics.
     f, type_from_typevar = _resolver.resolve_generic_types(f)
     f = _resolver.apply_type_from_typevar(f, type_from_typevar)
-    f = _resolver.narrow_subtypes(f, default_instance)
+    f = _resolver.unwrap_newtype_and_narrow_subtypes(f, default_instance)
     f = _resolver.narrow_collection_types(f, default_instance)
     f_origin = _resolver.unwrap_origin_strip_extras(cast(TypeForm, f))
 
@@ -647,12 +654,11 @@ def _field_list_from_pydantic(
             field_list.append(
                 FieldDefinition.make(
                     name=name,
-                    type_or_callable=pd2_field.annotation,  # type: ignore
-                    markers=tuple(
-                        meta
-                        for meta in pd2_field.metadata
-                        if isinstance(meta, _markers._Marker)
-                    ),
+                    type_or_callable=Annotated.__class_getitem__(  # type: ignore
+                        (pd2_field.annotation,) + tuple(pd2_field.metadata)
+                    )
+                    if len(pd2_field.metadata) > 0
+                    else pd2_field.annotation,
                     default=default,
                     helptext=helptext,
                 )
diff --git a/tyro/_instantiators.py b/src/tyro/_instantiators.py
similarity index 98%
rename from tyro/_instantiators.py
rename to src/tyro/_instantiators.py
index 84690b3f..0a7322f3 100644
--- a/tyro/_instantiators.py
+++ b/src/tyro/_instantiators.py
@@ -195,6 +195,14 @@ def instantiator(strings: List[str]) -> None:
     if container_out is not None:
         return container_out
 
+    # Unwrap NewType + set metavar based on NewType name.
+    # `isinstance(x, NewType)` doesn't work because NewType isn't a class until
+    # Python 3.10, so we instead do a duck typing-style check.
+    metavar = getattr(typ, "__name__", "").upper()
+    typ, maybe_newtype_name = _resolver.unwrap_newtype(typ)
+    if maybe_newtype_name is not None:
+        metavar = maybe_newtype_name.upper()
+
     # Validate that typ is a `(arg: str) -> T` type converter, as expected by argparse.
     if typ in _builtin_set:
         pass
@@ -246,7 +254,7 @@ def instantiator_base_case(strings: List[str]) -> Any:
     return instantiator_base_case, InstantiatorMetadata(
         nargs=1,
         metavar=(
-            typ.__name__.upper()
+            metavar
             if auto_choices is None
             else "{" + ",".join(map(str, auto_choices)) + "}"
         ),
diff --git a/tyro/_parsers.py b/src/tyro/_parsers.py
similarity index 99%
rename from tyro/_parsers.py
rename to src/tyro/_parsers.py
index f4f79c8f..2fd59a04 100644
--- a/tyro/_parsers.py
+++ b/src/tyro/_parsers.py
@@ -322,7 +322,7 @@ def handle_field(
         if _fields.is_nested_type(field.type_or_callable, field.default):
             field = dataclasses.replace(
                 field,
-                type_or_callable=_resolver.narrow_subtypes(
+                type_or_callable=_resolver.unwrap_newtype_and_narrow_subtypes(
                     field.type_or_callable,
                     field.default,
                 ),
diff --git a/tyro/_resolver.py b/src/tyro/_resolver.py
similarity index 87%
rename from tyro/_resolver.py
rename to src/tyro/_resolver.py
index c9007e7d..d8cd6243 100644
--- a/tyro/_resolver.py
+++ b/src/tyro/_resolver.py
@@ -12,6 +12,7 @@
     Dict,
     FrozenSet,
     List,
+    Optional,
     Set,
     Tuple,
     TypeVar,
@@ -33,10 +34,11 @@ def unwrap_origin_strip_extras(typ: TypeOrCallable) -> TypeOrCallable:
     # TODO: Annotated[] handling should be revisited...
     typ, _ = unwrap_annotated(typ)
     origin = get_origin(typ)
-    if origin is None:
-        return typ
-    else:
-        return origin
+
+    if origin is not None:
+        typ = origin
+
+    return typ
 
 
 def is_dataclass(cls: Union[TypeForm, Callable]) -> bool:
@@ -50,12 +52,15 @@ def resolve_generic_types(
     """If the input is a class: no-op. If it's a generic alias: returns the origin
     class, and a mapping from typevars to concrete types."""
 
-    origin_cls = get_origin(cls)
-    annotations = ()
-    if origin_cls is Annotated:
-        annotations = get_args(cls)[1:]
-        cls = get_args(cls)[0]
-        origin_cls = get_origin(cls)
+    annotations: Tuple[Any, ...] = ()
+    if get_origin(cls) is Annotated:
+        # ^We need this `if` statement for an obscure edge case: when `cls` is a
+        # function with `__tyro_markers__` set, we don't want/need to return
+        # Annotated[func, markers].
+        cls, annotations = unwrap_annotated(cls)
+
+    # We'll ignore NewType when getting the origin + args for generics.
+    origin_cls = get_origin(unwrap_newtype(cls)[0])
 
     type_from_typevar = {}
     if (
@@ -65,7 +70,7 @@ def resolve_generic_types(
         and hasattr(origin_cls.__parameters__, "__len__")
     ):
         typevars = origin_cls.__parameters__
-        typevar_values = get_args(cls)
+        typevar_values = get_args(unwrap_newtype(cls)[0])
         assert len(typevars) == len(typevar_values)
         cls = origin_cls
         type_from_typevar.update(dict(zip(typevars, typevar_values)))
@@ -83,7 +88,9 @@ def resolve_generic_types(
     if len(annotations) == 0:
         return cls, type_from_typevar
     else:
-        return Annotated.__class_getitem__((cls, *annotations)), type_from_typevar  # type: ignore
+        return Annotated.__class_getitem__(  # type: ignore
+            (cls, *annotations)
+        ), type_from_typevar
 
 
 @_unsafe_cache.unsafe_cache(maxsize=1024)
@@ -136,8 +143,31 @@ def type_from_typevar_constraints(typ: TypeOrCallable) -> TypeOrCallable:
     return typ
 
 
+TypeOrCallableOrNone = TypeVar("TypeOrCallableOrNone", Callable, TypeForm[Any], None)
+
+
+def unwrap_newtype(
+    typ: TypeOrCallableOrNone,
+) -> Tuple[TypeOrCallableOrNone, Optional[str]]:
+    # We'll unwrap NewType annotations here; this is needed before issubclass
+    # checks!
+    #
+    # `isinstance(x, NewType)` doesn't work because NewType isn't a class until
+    # Python 3.10, so we instead do a duck typing-style check.
+    return_name = None
+    while hasattr(typ, "__name__") and hasattr(typ, "__supertype__"):
+        if return_name is None:
+            return_name = getattr(typ, "__name__")
+        typ = getattr(typ, "__supertype__")
+
+    return typ, return_name
+
+
 @_unsafe_cache.unsafe_cache(maxsize=1024)
-def narrow_subtypes(typ: TypeOrCallable, default_instance: Any) -> TypeOrCallable:
+def unwrap_newtype_and_narrow_subtypes(
+    typ: TypeOrCallable,
+    default_instance: Any,
+) -> TypeOrCallable:
     """Type narrowing: if we annotate as Animal but specify a default instance of Cat,
     we should parse as Cat.
 
@@ -145,6 +175,10 @@ def narrow_subtypes(typ: TypeOrCallable, default_instance: Any) -> TypeOrCallabl
     individual arguments/fields. (if a field is annotated as Union[int, str], and a
     string default is passed in, we don't want to narrow the type to always be
     strings!)"""
+
+    typ, unused_name = unwrap_newtype(typ)
+    del unused_name
+
     try:
         potential_subclass = type(default_instance)
 
diff --git a/tyro/_singleton.py b/src/tyro/_singleton.py
similarity index 100%
rename from tyro/_singleton.py
rename to src/tyro/_singleton.py
diff --git a/tyro/_strings.py b/src/tyro/_strings.py
similarity index 100%
rename from tyro/_strings.py
rename to src/tyro/_strings.py
diff --git a/tyro/_subcommand_matching.py b/src/tyro/_subcommand_matching.py
similarity index 96%
rename from tyro/_subcommand_matching.py
rename to src/tyro/_subcommand_matching.py
index 8ba0fbf3..94cad1c3 100644
--- a/tyro/_subcommand_matching.py
+++ b/src/tyro/_subcommand_matching.py
@@ -97,9 +97,11 @@ def _get_type_options(typ: _typing.TypeForm) -> Tuple[_typing.TypeForm, ...]:
         # Check against supertypes.
         for self_type in self_types:
             self_type = _resolver.unwrap_annotated(self_type)[0]
+            self_type, _ = _resolver.unwrap_newtype(self_type)
             ok = False
             for super_type in super_types:
                 super_type = _resolver.unwrap_annotated(super_type)[0]
+                self_type, _ = _resolver.unwrap_newtype(self_type)
                 if issubclass(self_type, super_type):
                     ok = True
             if not ok:
diff --git a/tyro/_typing.py b/src/tyro/_typing.py
similarity index 100%
rename from tyro/_typing.py
rename to src/tyro/_typing.py
diff --git a/tyro/_unsafe_cache.py b/src/tyro/_unsafe_cache.py
similarity index 100%
rename from tyro/_unsafe_cache.py
rename to src/tyro/_unsafe_cache.py
diff --git a/tyro/conf/__init__.py b/src/tyro/conf/__init__.py
similarity index 100%
rename from tyro/conf/__init__.py
rename to src/tyro/conf/__init__.py
diff --git a/tyro/conf/_confstruct.py b/src/tyro/conf/_confstruct.py
similarity index 100%
rename from tyro/conf/_confstruct.py
rename to src/tyro/conf/_confstruct.py
diff --git a/tyro/conf/_markers.py b/src/tyro/conf/_markers.py
similarity index 100%
rename from tyro/conf/_markers.py
rename to src/tyro/conf/_markers.py
diff --git a/tyro/extras/__init__.py b/src/tyro/extras/__init__.py
similarity index 100%
rename from tyro/extras/__init__.py
rename to src/tyro/extras/__init__.py
diff --git a/tyro/extras/_base_configs.py b/src/tyro/extras/_base_configs.py
similarity index 100%
rename from tyro/extras/_base_configs.py
rename to src/tyro/extras/_base_configs.py
diff --git a/tyro/extras/_choices_type.py b/src/tyro/extras/_choices_type.py
similarity index 100%
rename from tyro/extras/_choices_type.py
rename to src/tyro/extras/_choices_type.py
diff --git a/tyro/extras/_serialization.py b/src/tyro/extras/_serialization.py
similarity index 100%
rename from tyro/extras/_serialization.py
rename to src/tyro/extras/_serialization.py
diff --git a/tyro/extras/_subcommand_cli_from_dict.py b/src/tyro/extras/_subcommand_cli_from_dict.py
similarity index 100%
rename from tyro/extras/_subcommand_cli_from_dict.py
rename to src/tyro/extras/_subcommand_cli_from_dict.py
diff --git a/tyro/py.typed b/src/tyro/py.typed
similarity index 100%
rename from tyro/py.typed
rename to src/tyro/py.typed


Test patch:
diff --git a/tests/test_generics_and_serialization.py b/tests/test_generics_and_serialization.py
index 5af75ee2..6f42315a 100644
--- a/tests/test_generics_and_serialization.py
+++ b/tests/test_generics_and_serialization.py
@@ -2,7 +2,7 @@
 import dataclasses
 import enum
 import io
-from typing import Generic, List, Tuple, Type, TypeVar, Union
+from typing import Generic, List, NewType, Tuple, Type, TypeVar, Union
 
 import pytest
 import yaml
@@ -30,6 +30,29 @@ class TupleGenericVariable(Generic[ScalarType]):
     ) == TupleGenericVariable((1, 2, 3))
 
 
+def test_tuple_generic_variable_newtype() -> None:
+    @dataclasses.dataclass
+    class TupleGenericVariable(Generic[ScalarType]):
+        xyz: Tuple[ScalarType, ...]
+
+    SpecialInt = NewType("SpecialInt", int)
+    assert tyro.cli(
+        TupleGenericVariable[SpecialInt], args=["--xyz", "1", "2", "3"]
+    ) == TupleGenericVariable((SpecialInt(1), SpecialInt(2), SpecialInt(3)))
+
+
+def test_tuple_generic_variable_more_newtype() -> None:
+    @dataclasses.dataclass
+    class TupleGenericVariable(Generic[ScalarType]):
+        xyz: Tuple[ScalarType, ...]
+
+    SpecialInt = NewType("SpecialInt", int)
+    SpecialTuple = NewType("SpecialTuple", TupleGenericVariable[SpecialInt])
+    assert tyro.cli(SpecialTuple, args=["--xyz", "1", "2", "3"]) == SpecialTuple(
+        TupleGenericVariable((SpecialInt(1), SpecialInt(2), SpecialInt(3)))
+    )
+
+
 def test_tuple_generic_helptext() -> None:
     @dataclasses.dataclass
     class TupleGenericVariableHelptext(Generic[ScalarType]):
diff --git a/tests/test_nested.py b/tests/test_nested.py
index 7d94ee6a..aa78b554 100644
--- a/tests/test_nested.py
+++ b/tests/test_nested.py
@@ -1,5 +1,5 @@
 import dataclasses
-from typing import Any, Generic, Mapping, Optional, Tuple, TypeVar, Union
+from typing import Any, Generic, Mapping, NewType, Optional, Tuple, TypeVar, Union
 
 import pytest
 from frozendict import frozendict  # type: ignore
@@ -170,6 +170,49 @@ class OptionalNested:
     ) == OptionalNested(x=1, b=OptionalNestedChild(y=2, z=3))
 
 
+def test_optional_nested_newtype() -> None:
+    @dataclasses.dataclass
+    class OptionalNestedChild:
+        y: int
+        z: int
+
+    SpecialOptionalNestedChild = NewType(
+        "SpecialOptionalNestedChild", OptionalNestedChild
+    )
+
+    @dataclasses.dataclass
+    class OptionalNested:
+        x: int
+        b: Optional[SpecialOptionalNestedChild] = None
+
+    assert tyro.cli(OptionalNested, args=["--x", "1"]) == OptionalNested(x=1, b=None)
+    with pytest.raises(SystemExit):
+        tyro.cli(
+            OptionalNested,
+            args=["--x", "1", "b:special-optional-nested-child", "--b.y", "3"],
+        )
+    with pytest.raises(SystemExit):
+        tyro.cli(
+            OptionalNested,
+            args=["--x", "1", "b:special-optional-nested-child", "--b.z", "3"],
+        )
+
+    assert tyro.cli(
+        OptionalNested,
+        args=[
+            "--x",
+            "1",
+            "b:special-optional-nested-child",
+            "--b.y",
+            "2",
+            "--b.z",
+            "3",
+        ],
+    ) == OptionalNested(
+        x=1, b=SpecialOptionalNestedChild(OptionalNestedChild(y=2, z=3))
+    )
+
+
 def test_optional_nested_multiple() -> None:
     """Adapted from: https://github.com/brentyi/tyro/issues/60"""
 
@@ -329,6 +372,56 @@ class DefaultSubparser:
         tyro.cli(DefaultSubparser, args=["--x", "1", "c", "--bc.y", "3"])
 
 
+def test_subparser_with_default_and_newtype() -> None:
+    @dataclasses.dataclass
+    class DefaultHTTPServer_:
+        y: int
+
+    DefaultHTTPServer__ = NewType("DefaultHTTPServer__", DefaultHTTPServer_)
+    DefaultHTTPServer = NewType("DefaultHTTPServer", DefaultHTTPServer__)
+
+    def make_http_server(y: int) -> DefaultHTTPServer:
+        return DefaultHTTPServer(DefaultHTTPServer__(DefaultHTTPServer_(y)))
+
+    @dataclasses.dataclass
+    class DefaultSMTPServer:
+        z: int
+
+    @dataclasses.dataclass
+    class DefaultSubparser:
+        x: int
+        bc: Union[DefaultHTTPServer, DefaultSMTPServer] = dataclasses.field(
+            default_factory=lambda: make_http_server(5)
+        )
+
+    assert (
+        tyro.cli(
+            DefaultSubparser, args=["--x", "1", "bc:default-http-server", "--bc.y", "5"]
+        )
+        == tyro.cli(DefaultSubparser, args=["--x", "1"])
+        == DefaultSubparser(x=1, bc=make_http_server(y=5))
+    )
+    assert tyro.cli(
+        DefaultSubparser, args=["--x", "1", "bc:default-smtp-server", "--bc.z", "3"]
+    ) == DefaultSubparser(x=1, bc=DefaultSMTPServer(z=3))
+    assert (
+        tyro.cli(
+            DefaultSubparser, args=["--x", "1", "bc:default-http-server", "--bc.y", "8"]
+        )
+        == tyro.cli(
+            DefaultSubparser,
+            args=[],
+            default=DefaultSubparser(x=1, bc=make_http_server(y=8)),
+        )
+        == DefaultSubparser(x=1, bc=make_http_server(y=8))
+    )
+
+    with pytest.raises(SystemExit):
+        tyro.cli(DefaultSubparser, args=["--x", "1", "b", "--bc.z", "3"])
+    with pytest.raises(SystemExit):
+        tyro.cli(DefaultSubparser, args=["--x", "1", "c", "--bc.y", "3"])
+
+
 def test_subparser_with_default_alternate() -> None:
     @dataclasses.dataclass
     class DefaultInstanceHTTPServer:
diff --git a/tests/test_py311_generated/test_attrs_generated.py b/tests/test_py311_generated/test_attrs_generated.py
index 42b0563a..2cd154a7 100644
--- a/tests/test_py311_generated/test_attrs_generated.py
+++ b/tests/test_py311_generated/test_attrs_generated.py
@@ -5,10 +5,10 @@
 
 import attr
 import pytest
-import tyro._strings
 from attrs import define, field
 
 import tyro
+import tyro._strings
 
 
 def test_attrs_basic() -> None:
diff --git a/tests/test_py311_generated/test_dict_namedtuple_generated.py b/tests/test_py311_generated/test_dict_namedtuple_generated.py
index 18be741c..44e56099 100644
--- a/tests/test_py311_generated/test_dict_namedtuple_generated.py
+++ b/tests/test_py311_generated/test_dict_namedtuple_generated.py
@@ -17,9 +17,9 @@
 )
 
 import pytest
-import tyro._strings
 
 import tyro
+import tyro._strings
 
 
 def test_basic_dict() -> None:
diff --git a/tests/test_py311_generated/test_errors_new_annotations_generated.py b/tests/test_py311_generated/test_errors_new_annotations_generated.py
new file mode 100644
index 00000000..75b69f58
--- /dev/null
+++ b/tests/test_py311_generated/test_errors_new_annotations_generated.py
@@ -0,0 +1,35 @@
+from __future__ import annotations
+
+import sys
+
+import pytest
+
+import tyro
+
+
+@pytest.mark.skipif(
+    sys.version_info >= (3, 10), reason="No error for newer versions of Python."
+)
+def test_new_union_error() -> None:
+    """PEP 604 allows `|` to be used as a type annotation in Python >=3.10."""
+
+    def main(x: int | str) -> None:
+        ...
+
+    with pytest.raises(TypeError) as e:
+        tyro.cli(main)
+    assert "You may be using a Union in the form of `X | Y`" in e.value.args[0]
+
+
+@pytest.mark.skipif(
+    sys.version_info >= (3, 9), reason="No error for newer versions of Python."
+)
+def test_new_collection_error() -> None:
+    """PEP 585 allows standard collections to be used as generics in Python >=3.9."""
+
+    def main(x: list[int]) -> None:
+        ...
+
+    with pytest.raises(TypeError) as e:
+        tyro.cli(main)
+    assert "You may be using a standard collection as a generic" in e.value.args[0]
diff --git a/tests/test_py311_generated/test_generics_and_serialization_generated.py b/tests/test_py311_generated/test_generics_and_serialization_generated.py
index 223601cd..a0897ef2 100644
--- a/tests/test_py311_generated/test_generics_and_serialization_generated.py
+++ b/tests/test_py311_generated/test_generics_and_serialization_generated.py
@@ -2,7 +2,7 @@
 import dataclasses
 import enum
 import io
-from typing import Annotated, Generic, List, Tuple, Type, TypeVar
+from typing import Annotated, Generic, List, NewType, Tuple, Type, TypeVar
 
 import pytest
 import yaml
@@ -29,6 +29,29 @@ class TupleGenericVariable(Generic[ScalarType]):
     ) == TupleGenericVariable((1, 2, 3))
 
 
+def test_tuple_generic_variable_newtype() -> None:
+    @dataclasses.dataclass
+    class TupleGenericVariable(Generic[ScalarType]):
+        xyz: Tuple[ScalarType, ...]
+
+    SpecialInt = NewType("SpecialInt", int)
+    assert tyro.cli(
+        TupleGenericVariable[SpecialInt], args=["--xyz", "1", "2", "3"]
+    ) == TupleGenericVariable((SpecialInt(1), SpecialInt(2), SpecialInt(3)))
+
+
+def test_tuple_generic_variable_more_newtype() -> None:
+    @dataclasses.dataclass
+    class TupleGenericVariable(Generic[ScalarType]):
+        xyz: Tuple[ScalarType, ...]
+
+    SpecialInt = NewType("SpecialInt", int)
+    SpecialTuple = NewType("SpecialTuple", TupleGenericVariable[SpecialInt])
+    assert tyro.cli(SpecialTuple, args=["--xyz", "1", "2", "3"]) == SpecialTuple(
+        TupleGenericVariable((SpecialInt(1), SpecialInt(2), SpecialInt(3)))
+    )
+
+
 def test_tuple_generic_helptext() -> None:
     @dataclasses.dataclass
     class TupleGenericVariableHelptext(Generic[ScalarType]):
diff --git a/tests/test_py311_generated/test_nested_generated.py b/tests/test_py311_generated/test_nested_generated.py
index 2b72d6cd..c72222f2 100644
--- a/tests/test_py311_generated/test_nested_generated.py
+++ b/tests/test_py311_generated/test_nested_generated.py
@@ -5,6 +5,7 @@
     Generic,
     Literal,
     Mapping,
+    NewType,
     Optional,
     Tuple,
     TypeVar,
@@ -178,6 +179,49 @@ class OptionalNested:
     ) == OptionalNested(x=1, b=OptionalNestedChild(y=2, z=3))
 
 
+def test_optional_nested_newtype() -> None:
+    @dataclasses.dataclass
+    class OptionalNestedChild:
+        y: int
+        z: int
+
+    SpecialOptionalNestedChild = NewType(
+        "SpecialOptionalNestedChild", OptionalNestedChild
+    )
+
+    @dataclasses.dataclass
+    class OptionalNested:
+        x: int
+        b: Optional[SpecialOptionalNestedChild] = None
+
+    assert tyro.cli(OptionalNested, args=["--x", "1"]) == OptionalNested(x=1, b=None)
+    with pytest.raises(SystemExit):
+        tyro.cli(
+            OptionalNested,
+            args=["--x", "1", "b:special-optional-nested-child", "--b.y", "3"],
+        )
+    with pytest.raises(SystemExit):
+        tyro.cli(
+            OptionalNested,
+            args=["--x", "1", "b:special-optional-nested-child", "--b.z", "3"],
+        )
+
+    assert tyro.cli(
+        OptionalNested,
+        args=[
+            "--x",
+            "1",
+            "b:special-optional-nested-child",
+            "--b.y",
+            "2",
+            "--b.z",
+            "3",
+        ],
+    ) == OptionalNested(
+        x=1, b=SpecialOptionalNestedChild(OptionalNestedChild(y=2, z=3))
+    )
+
+
 def test_optional_nested_multiple() -> None:
     """Adapted from: https://github.com/brentyi/tyro/issues/60"""
 
@@ -337,6 +381,56 @@ class DefaultSubparser:
         tyro.cli(DefaultSubparser, args=["--x", "1", "c", "--bc.y", "3"])
 
 
+def test_subparser_with_default_and_newtype() -> None:
+    @dataclasses.dataclass
+    class DefaultHTTPServer_:
+        y: int
+
+    DefaultHTTPServer__ = NewType("DefaultHTTPServer__", DefaultHTTPServer_)
+    DefaultHTTPServer = NewType("DefaultHTTPServer", DefaultHTTPServer__)
+
+    def make_http_server(y: int) -> DefaultHTTPServer:
+        return DefaultHTTPServer(DefaultHTTPServer__(DefaultHTTPServer_(y)))
+
+    @dataclasses.dataclass
+    class DefaultSMTPServer:
+        z: int
+
+    @dataclasses.dataclass
+    class DefaultSubparser:
+        x: int
+        bc: DefaultHTTPServer | DefaultSMTPServer = dataclasses.field(
+            default_factory=lambda: make_http_server(5)
+        )
+
+    assert (
+        tyro.cli(
+            DefaultSubparser, args=["--x", "1", "bc:default-http-server", "--bc.y", "5"]
+        )
+        == tyro.cli(DefaultSubparser, args=["--x", "1"])
+        == DefaultSubparser(x=1, bc=make_http_server(y=5))
+    )
+    assert tyro.cli(
+        DefaultSubparser, args=["--x", "1", "bc:default-smtp-server", "--bc.z", "3"]
+    ) == DefaultSubparser(x=1, bc=DefaultSMTPServer(z=3))
+    assert (
+        tyro.cli(
+            DefaultSubparser, args=["--x", "1", "bc:default-http-server", "--bc.y", "8"]
+        )
+        == tyro.cli(
+            DefaultSubparser,
+            args=[],
+            default=DefaultSubparser(x=1, bc=make_http_server(y=8)),
+        )
+        == DefaultSubparser(x=1, bc=make_http_server(y=8))
+    )
+
+    with pytest.raises(SystemExit):
+        tyro.cli(DefaultSubparser, args=["--x", "1", "b", "--bc.z", "3"])
+    with pytest.raises(SystemExit):
+        tyro.cli(DefaultSubparser, args=["--x", "1", "c", "--bc.y", "3"])
+
+
 def test_subparser_with_default_alternate() -> None:
     @dataclasses.dataclass
     class DefaultInstanceHTTPServer:
diff --git a/tests/test_py311_generated/test_pydantic_generated.py b/tests/test_py311_generated/test_pydantic_generated.py
index 92e37883..e60b8d3f 100644
--- a/tests/test_py311_generated/test_pydantic_generated.py
+++ b/tests/test_py311_generated/test_pydantic_generated.py
@@ -1,13 +1,13 @@
 import contextlib
 import io
 import pathlib
-from typing import cast
+from typing import Annotated, cast
 
 import pytest
-import tyro._strings
-from pydantic import BaseModel, Field
+from pydantic import BaseModel, Field, v1
 
 import tyro
+import tyro._strings
 
 
 def test_pydantic() -> None:
@@ -29,6 +29,25 @@ class ManyTypesA(BaseModel):
     ) == ManyTypesA(i=5, s="hello", f=3.0, p=pathlib.Path("~"))
 
 
+def test_pydantic_v1() -> None:
+    class ManyTypesA(v1.BaseModel):
+        i: int
+        s: str = "hello"
+        f: float = v1.Field(default_factory=lambda: 3.0)
+        p: pathlib.Path
+
+    # We can directly pass a dataclass to `tyro.cli()`:
+    assert tyro.cli(
+        ManyTypesA,
+        args=[
+            "--i",
+            "5",
+            "--p",
+            "~",
+        ],
+    ) == ManyTypesA(i=5, s="hello", f=3.0, p=pathlib.Path("~"))
+
+
 def test_pydantic_helptext() -> None:
     class Helptext(BaseModel):
         """This docstring should be printed as a description."""
@@ -107,3 +126,56 @@ class AnnotatedAsPositional(BaseModel):
 
     result = tyro.cli(AnnotatedAsPositional, args=["myname"])
     assert isinstance(result, AnnotatedAsPositional)
+
+
+def test_pydantic_alias() -> None:
+    class AliasCfg(BaseModel):
+        alias: Annotated[str, tyro.conf.arg(aliases=["-a"])]
+
+    assert tyro.cli(AliasCfg, args=["--alias", "3"]) == AliasCfg(alias="3")
+    assert tyro.cli(AliasCfg, args=["-a", "3"]) == AliasCfg(alias="3")
+
+
+def test_pydantic_default_instance() -> None:
+    class Inside(BaseModel):
+        x: int = 1
+
+    class Outside(BaseModel):
+        i: Inside = Inside(x=2)
+
+    assert tyro.cli(Outside, args=[]).i.x == 2, (
+        "Expected x value from the default instance",
+    )
+    assert tyro.cli(Outside, args=["--i.x", "3"]).i.x == 3
+
+
+def test_pydantic_nested_default_instance() -> None:
+    class Inside(BaseModel):
+        x: int = 1
+
+    class Middle(BaseModel):
+        i: Inside
+
+    class Outside(BaseModel):
+        m: Middle = Middle(i=Inside(x=2))
+
+    assert tyro.cli(Outside, args=[]).m.i.x == 2, (
+        "Expected x value from the default instance",
+    )
+    assert tyro.cli(Outside, args=["--m.i.x", "3"]).m.i.x == 3
+
+
+def test_pydantic_v1_nested_default_instance() -> None:
+    class Inside(v1.BaseModel):
+        x: int = 1
+
+    class Middle(v1.BaseModel):
+        i: Inside
+
+    class Outside(v1.BaseModel):
+        m: Middle = Middle(i=Inside(x=2))
+
+    assert tyro.cli(Outside, args=[]).m.i.x == 2, (
+        "Expected x value from the default instance",
+    )
+    assert tyro.cli(Outside, args=["--m.i.x", "3"]).m.i.x == 3
diff --git a/tests/test_py311_generated/test_pydantic_with_newtype_generated.py b/tests/test_py311_generated/test_pydantic_with_newtype_generated.py
new file mode 100644
index 00000000..1ac21745
--- /dev/null
+++ b/tests/test_py311_generated/test_pydantic_with_newtype_generated.py
@@ -0,0 +1,36 @@
+from typing import Annotated, NewType, Tuple
+
+import pydantic
+from pydantic import BaseModel
+
+import tyro
+
+Microliter = NewType("Microliter", int)
+
+
+class Measurements(BaseModel):
+    single: Microliter = pydantic.Field(10)
+    renamed_single: Annotated[
+        Microliter, tyro.conf.arg(name="other_single")
+    ] = pydantic.Field(10)
+    pair: Tuple[Microliter, Microliter] = pydantic.Field((20, 30))
+
+
+IncorrectMeasurements = NewType("IncorrectMeasurements", Measurements)
+
+
+def test_pydantic_with_newtype():
+    assert tyro.cli(
+        IncorrectMeasurements, args="--single 1 --pair 2 3".split(" ")
+    ) == Measurements(
+        single=Microliter(1),
+        renamed_single=Microliter(10),
+        pair=(Microliter(2), Microliter(3)),
+    )
+    assert tyro.cli(
+        IncorrectMeasurements, args="--single 1 --other-single 5".split(" ")
+    ) == Measurements(
+        single=Microliter(1),
+        renamed_single=Microliter(5),
+        pair=(Microliter(20), Microliter(30)),
+    )
diff --git a/tests/test_py311_generated/test_unsupported_but_should_work_generated.py b/tests/test_py311_generated/test_unsupported_but_should_work_generated.py
index 370833c4..7341d325 100644
--- a/tests/test_py311_generated/test_unsupported_but_should_work_generated.py
+++ b/tests/test_py311_generated/test_unsupported_but_should_work_generated.py
@@ -7,9 +7,9 @@
 
 import omegaconf
 import pytest
-import tyro._strings
 
 import tyro
+import tyro._strings
 
 
 def test_omegaconf_missing():
diff --git a/tests/test_pydantic.py b/tests/test_pydantic.py
index ab5cca63..8882404a 100644
--- a/tests/test_pydantic.py
+++ b/tests/test_pydantic.py
@@ -4,10 +4,11 @@
 from typing import cast
 
 import pytest
-import tyro._strings
 from pydantic import BaseModel, Field, v1
+from typing_extensions import Annotated
 
 import tyro
+import tyro._strings
 
 
 def test_pydantic() -> None:
@@ -128,6 +129,14 @@ class AnnotatedAsPositional(BaseModel):
     assert isinstance(result, AnnotatedAsPositional)
 
 
+def test_pydantic_alias() -> None:
+    class AliasCfg(BaseModel):
+        alias: Annotated[str, tyro.conf.arg(aliases=["-a"])]
+
+    assert tyro.cli(AliasCfg, args=["--alias", "3"]) == AliasCfg(alias="3")
+    assert tyro.cli(AliasCfg, args=["-a", "3"]) == AliasCfg(alias="3")
+
+
 def test_pydantic_default_instance() -> None:
     class Inside(BaseModel):
         x: int = 1
diff --git a/tests/test_pydantic_with_newtype.py b/tests/test_pydantic_with_newtype.py
new file mode 100644
index 00000000..9ec0252f
--- /dev/null
+++ b/tests/test_pydantic_with_newtype.py
@@ -0,0 +1,37 @@
+from typing import NewType, Tuple
+
+import pydantic
+from pydantic import BaseModel
+from typing_extensions import Annotated
+
+import tyro
+
+Microliter = NewType("Microliter", int)
+
+
+class Measurements(BaseModel):
+    single: Microliter = pydantic.Field(10)
+    renamed_single: Annotated[
+        Microliter, tyro.conf.arg(name="other_single")
+    ] = pydantic.Field(10)
+    pair: Tuple[Microliter, Microliter] = pydantic.Field((20, 30))
+
+
+IncorrectMeasurements = NewType("IncorrectMeasurements", Measurements)
+
+
+def test_pydantic_with_newtype():
+    assert tyro.cli(
+        IncorrectMeasurements, args="--single 1 --pair 2 3".split(" ")
+    ) == Measurements(
+        single=Microliter(1),
+        renamed_single=Microliter(10),
+        pair=(Microliter(2), Microliter(3)),
+    )
+    assert tyro.cli(
+        IncorrectMeasurements, args="--single 1 --other-single 5".split(" ")
+    ) == Measurements(
+        single=Microliter(1),
+        renamed_single=Microliter(5),
+        pair=(Microliter(20), Microliter(30)),
+    )