Repository: albumentations-team/albumentations

Issue:
`from_dict` defaults `p=0.5` too rigorous for backward compatibility
## Describe the bug

#2481 refactored the serialization interface and the `from_dict` method (from `albumentations/core/serialization.py`) now automatically defaults to `p=0.5` as a "backward compatibility feature" (see [here](https://github.com/albumentations-team/albumentations/blame/main/albumentations/core/serialization.py#L180)).

Importantly, this is not the default value for all transforms (e.g., `RandomRotate90`, `Resize`, `PadIfNeeded`, etc.).

### To Reproduce

Steps to reproduce the behavior:

albumentations==2.0.5:
`albumentations.from_dict({ 'transform': {'__class_fullname__': 'Resize',  'height': 224,  'width': 224}})`
`Out: Resize(p=1.0, height=224, width=224, interpolation=1, mask_interpolation=0)`

albumentations==2.0.8:
`albumentations.from_dict({ 'transform': {'__class_fullname__': 'Resize',  'height': 224,  'width': 224}})`
`Out: Resize(p=0.5, area_for_downscale=None, height=224, interpolation=1, mask_interpolation=0, width=224)`
In conjunction with a `UserWarning`: `Transform Resize has no 'p' parameter in serialized data, defaulting to 0.5`

### Expected behavior

I expect instantiation with default parameters for all kwargs (e.g., as it is with with  `interpolation` in the example above).

### Actual behavior

The `UserWarning` is raised and `p=0.5` is set (almost) independently of the actual transform and its usual default value.

### Screenshots

N/A

### Additional context

I use `from_dict` to compile the augmentation pipeline from config files, similar to the (outdated) [autoalbument](https://github.com/albumentations-team/autoalbument/blob/master/autoalbument/faster_autoaugment/datamodule.py#L58) package. To be concise in the config files I previously relied on default values. The update breaks backward compatibility with these config files (in my case the introduced randomness through `p=0.5` resulted in differently resized tensors that could not be collated by the pytorch Dataloader). Moreover, I see no reason to treat `p` so different than other kwargs of a transform.



Discussion:
Thanks, will look in the next couple days.
@godaup Fixed



Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 4520d276a..1cfcfe34a 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -70,7 +70,7 @@ repos:
         language: system
         files: setup.py
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.11.12
+    rev: v0.11.13
     hooks:
       - id: ruff
         exclude: '__pycache__/'
@@ -98,7 +98,7 @@ repos:
     hooks:
       - id: pyproject-fmt
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.15.0
+    rev: v1.16.0
     hooks:
       - id: mypy
         files: ^albumentations/
diff --git a/albumentations/augmentations/mixing/domain_adaptation.py b/albumentations/augmentations/mixing/domain_adaptation.py
index c64cfffed..cdb8dd545 100644
--- a/albumentations/augmentations/mixing/domain_adaptation.py
+++ b/albumentations/augmentations/mixing/domain_adaptation.py
@@ -169,7 +169,8 @@ class BaseDomainAdaptation(ImageOnlyTransform):
 
     """
 
-    InitSchema: type[BaseDomainAdaptationInitSchema]
+    class InitSchema(BaseDomainAdaptationInitSchema):
+        pass
 
     def __init__(
         self,
diff --git a/albumentations/core/serialization.py b/albumentations/core/serialization.py
index 781e2950a..1cceccf4e 100644
--- a/albumentations/core/serialization.py
+++ b/albumentations/core/serialization.py
@@ -35,6 +35,9 @@
 SERIALIZABLE_REGISTRY: dict[str, SerializableMeta] = {}
 NON_SERIALIZABLE_REGISTRY: dict[str, SerializableMeta] = {}
 
+# Cache for default p values to avoid repeated inspect.signature calls
+_default_p_cache: dict[type, float] = {}
+
 
 def shorten_class_name(class_fullname: str) -> str:
     # Split the class_fullname once at the last '.' to separate the class name
@@ -174,14 +177,38 @@ def from_dict(
     name = transform["__class_fullname__"]
     args = {k: v for k, v in transform.items() if k != "__class_fullname__"}
 
-    # Ensure 'p' is included, default to 0.5 if missing for backward compatibility
-    if "p" not in args and name not in ("Compose", "Sequential"):
-        warn(f"Transform {name} has no 'p' parameter in serialized data, defaulting to 0.5", stacklevel=2)
-        args["p"] = 0.5
-
+    # Get the transform class from registry
     cls = SERIALIZABLE_REGISTRY[shorten_class_name(name)]
+
+    # Handle missing 'p' parameter for backward compatibility
+    if "p" not in args:
+        # Import here to avoid circular imports
+        from albumentations.core.composition import BaseCompose
+
+        # Check if it's a composition class by verifying if it is a subclass of BaseCompose
+        if not issubclass(cls, BaseCompose):
+            # Check if default 'p' value is cached
+            if cls not in _default_p_cache:
+                # Use inspect to get the default value of p from __init__
+                import inspect
+
+                sig = inspect.signature(cls.__init__)
+                p_param = sig.parameters.get("p")
+                default_p = p_param.default if p_param and p_param.default != inspect.Parameter.empty else 0.5
+                _default_p_cache[cls] = default_p
+            else:
+                default_p = _default_p_cache[cls]
+
+            warn(
+                f"Transform {cls.__name__} has no 'p' parameter in serialized data, defaulting to {default_p}",
+                stacklevel=2,
+            )
+            args["p"] = default_p
+
+    # Handle nested transforms
     if "transforms" in args:
         args["transforms"] = [from_dict({"transform": t}, nonserializable=nonserializable) for t in args["transforms"]]
+
     return cls(**args)
 
 


Test patch:
diff --git a/tests/test_serialization_backward_compatibility.py b/tests/test_serialization_backward_compatibility.py
new file mode 100644
index 000000000..7fce68e1f
--- /dev/null
+++ b/tests/test_serialization_backward_compatibility.py
@@ -0,0 +1,143 @@
+import pytest
+import warnings
+import albumentations as A
+
+
+def test_from_dict_without_p_uses_correct_defaults():
+    """Test that from_dict uses correct default p values when p is missing."""
+
+    # Test transform with p=1.0 default (Resize)
+    config = {
+        "transform": {
+            "__class_fullname__": "Resize",
+            "height": 224,
+            "width": 224
+        }
+    }
+
+    with pytest.warns(UserWarning, match="Transform Resize has no 'p' parameter.*defaulting to 1"):
+        transform = A.from_dict(config)
+
+    assert transform.p == 1.0, "Resize should have p=1.0 when not specified"
+
+    # Test transform with p=0.5 default (HorizontalFlip)
+    config = {
+        "transform": {
+            "__class_fullname__": "HorizontalFlip"
+        }
+    }
+
+    with pytest.warns(UserWarning, match="Transform HorizontalFlip has no 'p' parameter.*defaulting to 0.5"):
+        transform = A.from_dict(config)
+
+    assert transform.p == 0.5, "HorizontalFlip should have p=0.5 when not specified"
+
+
+def test_compose_without_p_no_warning():
+    """Test that Compose gets default p=1.0 when p is missing."""
+
+    config = {
+        "transform": {
+            "__class_fullname__": "Compose",
+            "transforms": []
+        }
+    }
+
+    # Should not produce warning because Compose inherits from BaseCompose
+    # which is handled specially in from_dict
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")  # Turn warnings into errors
+        transform = A.from_dict(config)
+
+    # Compose actually does have a p parameter with default 1.0
+    assert transform.p == 1.0
+
+
+def test_nested_transforms_without_p():
+    """Test that nested transforms get correct p values when missing."""
+
+    config = {
+        "transform": {
+            "__class_fullname__": "Compose",
+            "transforms": [
+                {
+                    "__class_fullname__": "Resize",
+                    "height": 224,
+                    "width": 224
+                },
+                {
+                    "__class_fullname__": "RandomBrightnessContrast"
+                },
+                {
+                    "__class_fullname__": "Normalize"
+                }
+            ]
+        }
+    }
+
+    with pytest.warns(UserWarning) as warnings_list:
+        transform = A.from_dict(config)
+
+    # Should have 3 warnings for the 3 transforms missing p
+    assert len(warnings_list) == 3
+
+    # Check each transform has correct p value
+    assert transform.transforms[0].p == 1.0  # Resize
+    assert transform.transforms[1].p == 0.5  # RandomBrightnessContrast
+    assert transform.transforms[2].p == 1.0  # Normalize
+
+
+@pytest.mark.parametrize("transform_name,expected_p", [
+    ("Resize", 1.0),
+    ("PadIfNeeded", 1.0),
+    ("CenterCrop", 1.0),
+    ("RandomCrop", 1.0),
+    ("Normalize", 1.0),
+    ("ToFloat", 1.0),
+    ("FromFloat", 1.0),
+    ("HorizontalFlip", 0.5),
+    ("VerticalFlip", 0.5),
+    ("RandomBrightnessContrast", 0.5),
+    ("GaussNoise", 0.5),
+    ("Blur", 0.5),
+])
+def test_various_transforms_default_p(transform_name, expected_p):
+    """Test that various transforms get their correct default p values."""
+
+    # Build minimal config for each transform
+    config = {"transform": {"__class_fullname__": transform_name}}
+
+    # Add required params for some transforms
+    if transform_name == "Resize":
+        config["transform"].update({"height": 224, "width": 224})
+    elif transform_name == "PadIfNeeded":
+        config["transform"].update({"min_height": 512, "min_width": 512})
+    elif transform_name in ["CenterCrop", "RandomCrop"]:
+        config["transform"].update({"height": 100, "width": 100})
+    elif transform_name == "FromFloat":
+        config["transform"].update({"dtype": "uint8"})
+
+    with pytest.warns(UserWarning, match=f"Transform {transform_name} has no 'p' parameter"):
+        transform = A.from_dict(config)
+
+    assert transform.p == expected_p, f"{transform_name} should have p={expected_p} when not specified"
+
+
+def test_transform_with_explicit_p_no_warning():
+    """Test that no warning is produced when p is explicitly provided."""
+
+    config = {
+        "transform": {
+            "__class_fullname__": "Resize",
+            "height": 224,
+            "width": 224,
+            "p": 0.8  # Explicitly provided
+        }
+    }
+
+    # Should not produce any warnings
+    with warnings.catch_warnings():
+        warnings.simplefilter("error")  # Turn warnings into errors
+        transform = A.from_dict(config)
+
+    assert transform.p == 0.8