The main difference between the two is actually the difference between these two types:
def pet: P = p
// and
def pet: Pet = p
So, in the first example, the type of pet is P, and in the second example, the type of pet is Pet. In other words: in the first example, the type is more precise, since it is a specific subtype of Pet.
If I put a Dog in the PetContainer[Dog], I get a Dog back out. Whereas, if I put a Dog in the PetContainer1, I get back a Pet, which could be either a Cat or a Dog, or something else entirely. (You haven't made your classes sealed or final, so someone else could come along, and create their own subclass of Pet.)
If you use an editor or IDE which shows you the types of expressions, or you try your code in the REPL or in Scastie, you actually see the difference:
dogContainer.pet // has type `Dog`
catContainer.pet // has type `Cat`
dogContainer1.pet // has type `Pet`
catContainer1.pet // has type `Pet`
See Scastie link for an example:
sealed trait Animal:
val name: String
sealed trait Pet extends Animal
case object Cat extends Pet:
override val name = "Cat"
case object Dog extends Pet:
override val name = "Dog"
case object Lion extends Animal:
override val name = "Lion"
final case class PetContainer[+P <: Pet](pet: P)
val dogContainer = PetContainer(Dog)
val catContainer = PetContainer(Cat)
final case class PetContainer1(pet: Pet)
val dogContainer1 = PetContainer1(Dog)
val catContainer1 = PetContainer1(Cat)
dogContainer.pet //=> Dog: Dog
catContainer.pet //=> Cat: Cat
dogContainer1.pet //=> Dog: Pet
catContainer1.pet //=> Cat: Pet
This gets more interesting if, say, you have a kennel that can hold two pets:
sealed trait Pet:
val name: String
case object Cat extends Pet:
override val name = "Cat"
case object Mouse extends Pet:
override val name = "Mouse"
final case class UnsafePetKennel(pet1: Pet, pet2: Pet)
val unsafeCatKennel = UnsafePetKennel(Cat, Cat)
val unsafeMouseKennel = UnsafePetKennel(Mouse, Mouse)
val oopsSomeoneAteMyMouse = UnsafePetKennel(Cat, Mouse)
final case class SafePetKennel[+P <: Pet](pet1: P, pet2: P)
val safeCatKennel = SafePetKennel[Cat.type](Cat, Cat)
val safeMouseKennel = SafePetKennel[Mouse.type](Mouse, Mouse)
val mouseIsSafe = SafePetKennel[Cat.type](Cat, Mouse)
// Type error: found `Mouse`, required `Cat`
val mouseStillSafe = SafePetKennel[Mouse.type](Cat, Mouse)
// Type error: found `Cat`, required `Mouse`
Scastie link
Unfortunately, things can still go wrong if we rely on Scala's type parameter inference:
val inferredKennel = SafePetKennel(Cat, Mouse)
works, because Scala infers P to be the least upper bound of Cat and Mouse, which is Pet, and thus inferredKennel has type SafePetKennel[Pet], and since both Cat and Mouse are subtypes of Pet, this is allowed.
We can fix that with a slight modification to our SafePetKennel:
final case class SaferKennel[+P <: Pet, +Q <: Pet](
pet1: P, pet2: Q)(implicit ev: P =:= Q)
val inferredKennel = SaferKennel(Cat, Mouse)
// Cannot prove that Cat =:= Mouse.
Scastie link