Repository: keras-team/keras

Issue:
Nested sequentials broken in 3.5
The following reduced example gives an error in Keras 3.5.0 but worked in 3.4.1 and earlier:

```python
import keras

model = keras.Sequential()
model.add(keras.Input(shape=(16,)))
keras.Sequential([model])
```

```
ValueError: Sequential model 'sequential' has no defined input shape yet.
```

The cause is the introduction of the `hasattr` call to check for the `input_shape` property here: https://github.com/keras-team/keras/blob/fa6be07ce86b3f8c5072a3d5ff355b4266baa32d/keras/src/models/sequential.py#L141

Since `hasattr` simply calls `getattr` and checks for an `AttributeError`, this call to `hasattr` actually raises a `ValueError` as though we were trying to call `.input_shape` instead of returning false like was intended.


Discussion:
Are you satisfied with the resolution of your issue?
<a href="https://docs.google.com/forms/d/e/1FAIpQLSdHag0RVFS7UXzZkKcsFCKOcX8raCupKK9RHSlYxp5U8lSJbQ/viewform?entry.492125872=Yes&entry.243948740=https://github.com/keras-team/keras/issues/20203">Yes</a>
<a href="https://docs.google.com/forms/d/e/1FAIpQLSdHag0RVFS7UXzZkKcsFCKOcX8raCupKK9RHSlYxp5U8lSJbQ/viewform?entry.492125872=No&entry.243948740=https://github.com/keras-team/keras/issues/20203">No</a>




Patch:
diff --git a/keras/src/models/sequential.py b/keras/src/models/sequential.py
index c0ed98fb854b..cdf5687a430c 100644
--- a/keras/src/models/sequential.py
+++ b/keras/src/models/sequential.py
@@ -268,7 +268,7 @@ def compute_output_shape(self, input_shape):
     def input_shape(self):
         if self._functional:
             return self._functional.input_shape
-        raise ValueError(
+        raise AttributeError(
             f"Sequential model '{self.name}' has no defined input shape yet."
         )
 
@@ -276,7 +276,7 @@ def input_shape(self):
     def output_shape(self):
         if self._functional:
             return self._functional.output_shape
-        raise ValueError(
+        raise AttributeError(
             f"Sequential model '{self.name}' has no defined output shape yet."
         )
 
@@ -284,7 +284,7 @@ def output_shape(self):
     def inputs(self):
         if self._functional:
             return self._functional.inputs
-        raise ValueError(
+        raise AttributeError(
             f"Sequential model '{self.name}' has no defined inputs yet."
         )
 
@@ -292,7 +292,7 @@ def inputs(self):
     def outputs(self):
         if self._functional:
             return self._functional.outputs
-        raise ValueError(
+        raise AttributeError(
             f"Sequential model '{self.name}' has no defined outputs yet."
         )
 


Test patch:
diff --git a/keras/src/models/sequential_test.py b/keras/src/models/sequential_test.py
index 943f6723b550..5e4078042039 100644
--- a/keras/src/models/sequential_test.py
+++ b/keras/src/models/sequential_test.py
@@ -227,6 +227,12 @@ def call(self, inputs):
         self.assertEqual(type(y), list)
         model.summary()
 
+    def test_nested_sequential(self):
+        # https://github.com/keras-team/keras/issues/20203
+        model = Sequential()
+        model.add(Input(shape=(16,)))
+        Sequential([model])
+
     def test_errors(self):
         # Trying to pass 2 Inputs
         model = Sequential()
@@ -359,3 +365,16 @@ def test_compute_output_shape(self):
         layer = Sequential([layers.Dense(4), layers.Dense(8)])
         output_shape = layer.compute_output_shape((1, 2))
         self.assertEqual(output_shape, (1, 8))
+
+    def test_hasattr(self):
+        model = Sequential()
+        self.assertFalse(hasattr(model, "input_shape"))
+        self.assertFalse(hasattr(model, "output_shape"))
+        self.assertFalse(hasattr(model, "inputs"))
+        self.assertFalse(hasattr(model, "outputs"))
+
+        model = Sequential([layers.Input((4,)), layers.Dense(8)])
+        self.assertTrue(hasattr(model, "input_shape"))
+        self.assertTrue(hasattr(model, "output_shape"))
+        self.assertTrue(hasattr(model, "inputs"))
+        self.assertTrue(hasattr(model, "outputs"))