tyro icon indicating copy to clipboard operation
tyro copied to clipboard

Uninitialized dataclass fields should be initialized with given default instance value

Open KolinGuo opened this issue 1 month ago • 3 comments

Hi @brentyi, thanks for your amazing repo again! I've noticed the following phenomenon and found it unexpected.

When a dataclass's field is marked with init=False, even if a default dataclass instance is passed in with a specified value at that field, the result from tyro.cli will not contain the field's value from the default instance. Instead, the field will still be left as uninitialized and trigger AttributeError on future access.

I found this to be counter-intuitive. Please see the minimal example below.

from dataclasses import dataclass, field

import tyro


@dataclass
class Conv1dConfig:
    in_channel: int = field(init=False)
    out_channel: int = 3


if __name__ == "__main__":
    default = Conv1dConfig()
    default.in_channel = 10

    config = tyro.cli(Conv1dConfig, default=default, use_underscores=True)
    assert config.in_channel == 10
    print(config)

It seems like this line filtered out fields with init=False before trying to check default values.

KolinGuo avatar Nov 23 '25 06:11 KolinGuo

Hi @KolinGuo, thanks for the issue + example! I just made a PR with a fix. Would you be able to check if it works for you?

brentyi avatar Nov 23 '25 08:11 brentyi

Hi @brentyi, thanks for your quick fix! I can confirm that the issue is resolved in your PR #391.

BTW, what's your take to add the fields with init=False and appears in defaults into the arg parser as well? It seems more semantically correct, but requires more handling after instantiating the dataclass since init=False fields cannot be constructed in your current functool.partial implementation.

KolinGuo avatar Nov 23 '25 08:11 KolinGuo

I think that's reasonable; just updated the branch!

Do you have thoughts on if fields should be overridable if:

  • init=False is set for field_name
  • default.field_name raises an AttributeError (eg is not set) ?

This is also doable, we can have the field "unset by default" but overridable. But there are likely some edge cases associated with generating subcommands from fields like this which I would need to work through.

brentyi avatar Nov 23 '25 09:11 brentyi

Making fields overridable in those cases would be fine. But it contradicts to the meaning of init=False IMHO.

  • init=False means the fields should not be initialized at instantiation (first-time constructed) time, but can be set later via direct field assignment.
  • tyro.cli accepts a type definition of the dataclass instead of a dataclass instance. In this sense, it's instantiating a new dataclass instance. So fields marked with init=False should not be initialized (and therefore not overridable).

However, if user passed in a default dataclass instance, it's different from first-time construction. So it's expected that existing init=False fields of the default instance get copied over and user can override the field values from command line.

KolinGuo avatar Nov 23 '25 19:11 KolinGuo

If you want to provide user with more ways to customize tyro's behavior, you could maybe add a conf flag for this. If the fiag (say, tyro.conf.AllowInitFalseFields) is specified, then tyro.cli will also provide args for fields that are

  • marked with init=False, AND
  • hasattr(default, field.name) == False

If the flag is not specified (the default case), tyro.cli will provide args for fields that are

  • not marked with init=False, OR
  • hasattr(default, field.name) == True

Personally, as long as the default case is correctly implemented, the behavior feels right to me.

KolinGuo avatar Nov 23 '25 20:11 KolinGuo

That makes sense. We can just start with the simplest thing for now:

  • Case 1: value in tyro.cli(default=...) → CLI flag
  • Case 2: value not in tyro.cli(default=...) → no CLI flag
    • Including when a default is set in dataclasses.field(default=...)

If the need arises in the future it's easy to either change the default behavior of case 2 or add a tyro.conf flag, since these don't require breaking changes.

brentyi avatar Nov 24 '25 08:11 brentyi