diff --git a/pyhmmer/daemon.pyi b/pyhmmer/daemon.pyi
index 727612b76b2d7642e9e46fb690e5f7760e17f26c..4a14b4a250ee1039e7606b54ab97592c8752d8f8 100644
--- a/pyhmmer/daemon.pyi
+++ b/pyhmmer/daemon.pyi
@@ -13,6 +13,8 @@ from pyhmmer.plan7 import TopHits, HMM, Builder
 
 BIT_CUTOFFS = Literal["gathering", "trusted", "noise"]
 
+S = typing.TypeVar("S", bound=Sequence)
+
 class Client:
     address: str
     port: int
@@ -30,7 +32,7 @@ class Client:
     def close(self) -> None: ...
     def search_seq(
         self,
-        query: Sequence,
+        query: S,
         db: int = 1,
         ranges: typing.Optional[typing.List[typing.Tuple[int, int]]] = None,
         *,
@@ -51,7 +53,7 @@ class Client:
         incdomE: float = 0.01,
         incdomT: typing.Optional[float] = None,
         bit_cutoffs: typing.Optional[BIT_CUTOFFS] = None,
-    ) -> TopHits: ...
+    ) -> TopHits[S]: ...
     def search_hmm(
         self,
         query: HMM,
@@ -75,10 +77,10 @@ class Client:
         incdomE: float = 0.01,
         incdomT: typing.Optional[float] = None,
         bit_cutoffs: typing.Optional[BIT_CUTOFFS] = None,
-    ) -> TopHits: ...
+    ) -> TopHits[HMM]: ...
     def scan_seq(
         self,
-        query: Sequence,
+        query: S,
         db: int = 1,
         *,
         bias_filter: bool = True,
@@ -98,14 +100,14 @@ class Client:
         incdomE: float = 0.01,
         incdomT: typing.Optional[float] = None,
         bit_cutoffs: typing.Optional[BIT_CUTOFFS] = None,
-    ) -> TopHits: ...
+    ) -> TopHits[S]: ...
     def iterate_seq(
         self,
         query: Sequence,
         db: int = 1,
         ranges: typing.Optional[typing.List[typing.Tuple[int, int]]] = None,
         builder: typing.Optional[Builder] = None,
-        select_hits: typing.Optional[typing.Callable[[TopHits], None]] = None,
+        select_hits: typing.Optional[typing.Callable[[TopHits[HMM]], None]] = None,
         *,
         bias_filter: bool = True,
         null2: bool = True,
@@ -131,7 +133,7 @@ class Client:
         db: int = 1,
         ranges: typing.Optional[typing.List[typing.Tuple[int, int]]] = None,
         builder: typing.Optional[Builder] = None,
-        select_hits: typing.Optional[typing.Callable[[TopHits], None]] = None,
+        select_hits: typing.Optional[typing.Callable[[TopHits[HMM]], None]] = None,
         *,
         bias_filter: bool = True,
         null2: bool = True,
@@ -162,6 +164,6 @@ class IterativeSearch(pyhmmer.plan7.IterativeSearch):
         db: int,
         builder: Builder,
         ranges: typing.Optional[typing.List[typing.Tuple[int, int]]] = None,
-        select_hits: typing.Optional[typing.Callable[[TopHits], None]] = None,
+        select_hits: typing.Optional[typing.Callable[[TopHits[HMM]], None]] = None,
         options: typing.Optional[typing.Dict[str, object]] = None,
     ) -> None: ...
diff --git a/pyhmmer/plan7.pyi b/pyhmmer/plan7.pyi
index 4e8023c8c312528e7fa755225d1c98ce3f4d1fb4..8f36d431b9f6f79105501bfbaccfb06cd23b3810 100644
--- a/pyhmmer/plan7.pyi
+++ b/pyhmmer/plan7.pyi
@@ -6,6 +6,7 @@ import os
 import sys
 import types
 import typing
+from typing import Any
 
 try:
     from typing import Literal
@@ -41,6 +42,8 @@ STRAND_SIGN = Literal["+", "-"]
 HITS_FORMAT = Literal["targets", "domain", "pfam"]
 HITS_MODE = Literal["search", "scan"]
 
+Q = typing.TypeVar("Q")  # pipeline query type
+
 class Alignment(collections.abc.Sized):
     domain: Domain
     def __len__(self) -> int: ...
@@ -277,7 +280,7 @@ class EvalueParameters:
     def as_vector(self) -> VectorF: ...
 
 class Hit(object):
-    hits: TopHits
+    hits: TopHits[Any]
     def __getstate__(self) -> typing.Dict[str, object]: ...
     @property
     def name(self) -> bytes: ...
@@ -460,7 +463,7 @@ class HMMPressedFile(typing.Iterator[OptimizedProfile]):
 
 class IterationResult(typing.NamedTuple):
     hmm: HMM
-    hits: TopHits
+    hits: TopHits[HMM]
     msa: DigitalMSA
     converged: bool
     iteration: int
@@ -479,7 +482,7 @@ class IterativeSearch(typing.Iterator[IterationResult]):
         builder: Builder,
         query: typing.Union[DigitalSequence, HMM],
         targets: DigitalSequenceBlock,
-        select_hits: typing.Optional[typing.Callable[[TopHits], None]] = None,
+        select_hits: typing.Optional[typing.Callable[[TopHits[HMM]], None]] = None,
     ) -> None: ...
     def __iter__(self) -> IterativeSearch: ...
     def __next__(self) -> IterationResult: ...
@@ -726,37 +729,37 @@ class Pipeline(object):
         self,
         query: typing.Union[HMM, Profile, OptimizedProfile],
         sequences: typing.Union[DigitalSequenceBlock, SequenceFile],
-    ) -> TopHits: ...
+    ) -> TopHits[typing.Union[HMM, Profile, OptimizedProfile]]: ...
     def search_msa(
         self,
         query: DigitalMSA,
         sequences: typing.Union[DigitalSequenceBlock, SequenceFile],
         builder: typing.Optional[Builder] = None,
-    ) -> TopHits: ...
+    ) -> TopHits[DigitalMSA]: ...
     def search_seq(
         self,
         query: DigitalSequence,
         sequences: typing.Union[DigitalSequenceBlock, SequenceFile],
         builder: typing.Optional[Builder] = None,
-    ) -> TopHits: ...
+    ) -> TopHits[DigitalSequence]: ...
     def scan_seq(
         self,
         query: DigitalSequence,
         optimized_profiles: typing.Union[OptimizedProfileBlock, HMMPressedFile],
-    ) -> TopHits: ...
+    ) -> TopHits[DigitalSequence]: ...
     def iterate_seq(
         self,
         query: DigitalSequence,
         sequences: DigitalSequenceBlock,
         builder: typing.Optional[Builder] = None,
-        select_hits: typing.Optional[typing.Callable[[TopHits], None]] = None,
+        select_hits: typing.Optional[typing.Callable[[TopHits[DigitalSequence]], None]] = None,
     ) -> IterativeSearch: ...
     def iterate_hmm(
         self,
         query: HMM,
         sequences: DigitalSequenceBlock,
         builder: typing.Optional[Builder] = None,
-        select_hits: typing.Optional[typing.Callable[[TopHits], None]] = None,
+        select_hits: typing.Optional[typing.Callable[[TopHits[DigitalSequence]], None]] = None,
     ) -> IterativeSearch: ...
 
 class LongTargetsPipeline(Pipeline):
@@ -853,17 +856,17 @@ class ScoreData(object):
     def __copy__(self) -> ScoreData: ...
     def copy(self) -> ScoreData: ...
 
-class TopHits(typing.Sequence[Hit]):
-    def __init__(self) -> None: ...
+class TopHits(typing.Sequence[Hit], typing.Generic[Q]):
+    def __init__(self, query: Q) -> None: ...
     def __bool__(self) -> bool: ...
-    def __copy__(self) -> TopHits: ...
-    def __deepcopy__(self, memo: typing.Dict[int, object]) -> TopHits: ...
+    def __copy__(self) -> TopHits[Q]: ...
+    def __deepcopy__(self, memo: typing.Dict[int, object]) -> TopHits[Q]: ...
     def __len__(self) -> int: ...
     @typing.overload
     def __getitem__(self, index: int) -> Hit: ...
     @typing.overload
     def __getitem__(self, index: slice) -> typing.Sequence[Hit]: ...
-    def __iadd__(self, other: TopHits) -> TopHits: ...
+    def __iadd__(self, other: TopHits[Q]) -> TopHits[Q]: ...
     def __getstate__(self) -> typing.Dict[str, object]: ...
     def __setstate__(self, state: typing.Dict[str, object]) -> None: ...
     @property
@@ -875,7 +878,7 @@ class TopHits(typing.Sequence[Hit]):
     @property
     def query_length(self) -> int: ...
     @property
-    def query(self) -> typing.Union[DigitalSequence, DigitalMSA, HMM, Profile, OptimizedProfile]: ...
+    def query(self) -> Q: ...
     @property
     def Z(self) -> float: ...
     @property
@@ -919,8 +922,8 @@ class TopHits(typing.Sequence[Hit]):
     def compare_ranking(self, ranking: KeyHash) -> int: ...
     def sort(self, by: SORT_KEY = "key") -> None: ...
     def is_sorted(self, by: SORT_KEY = "key") -> bool: ...
-    def copy(self) -> TopHits: ...
-    def merge(self, *others: TopHits) -> TopHits: ...
+    def copy(self) -> TopHits[Q]: ...
+    def merge(self, *others: TopHits[Q]) -> TopHits[Q]: ...
     def to_msa(
         self,
         alphabet: Alphabet,