Repository: quora/pyanalyze
Pull request #516 (2022-04-11 22:07:18)

Infer SequenceValue for sequences
Continues #515


Patch:
diff --git a/docs/changelog.md b/docs/changelog.md
index 0a3d696..805dd9a 100644
--- a/docs/changelog.md
+++ b/docs/changelog.md
@@ -2,6 +2,10 @@
 
 ## Unreleased
 
+- Initial support for variable-length heterogeneous sequences
+  (required for PEP 646). More precise types are now inferred
+  for heterogeneous sequences containing variable-length
+  objects. (#515, #516)
 - Support `LiteralString` (PEP 675) (#514)
 - Add `unused_assignment` error code, separated out from
   `unused_variable`. Enable these error codes and
diff --git a/pyanalyze/annotations.py b/pyanalyze/annotations.py
index 58b77fd..67f2805 100644
--- a/pyanalyze/annotations.py
+++ b/pyanalyze/annotations.py
@@ -63,7 +63,13 @@ from .extensions import (
 from .find_unused import used
 from .functions import FunctionDefNode
 from .node_visitor import ErrorContext
-from .signature import ELLIPSIS_PARAM, SigParameter, Signature, ParameterKind
+from .signature import (
+    ELLIPSIS_PARAM,
+    InvalidSignature,
+    SigParameter,
+    Signature,
+    ParameterKind,
+)
 from .safe import is_typing_name, is_instance_of_typing_name
 from . import type_evaluation
 from .value import (
@@ -85,7 +91,6 @@ from .value import (
     SequenceValue,
     TypeGuardExtension,
     TypedValue,
-    SequenceIncompleteValue,
     annotate_value,
     unite_values,
     Value,
@@ -385,7 +390,7 @@ def _type_from_runtime(
         origin = get_origin(val)
         args = get_args(val)
         if origin is tuple and not args:
-            return SequenceIncompleteValue(tuple, [])
+            return SequenceValue(tuple, [])
         return _value_of_origin_args(
             origin, args, val, ctx, unpack_allowed=origin is tuple
         )
@@ -404,24 +409,16 @@ def _type_from_runtime(
             if val is tuple or val is Tuple:
                 return TypedValue(tuple)
             else:
-                return SequenceIncompleteValue(tuple, [])
+                return SequenceValue(tuple, [])
         elif len(args) == 2 and args[1] is Ellipsis:
             return GenericValue(tuple, [_type_from_runtime(args[0], ctx)])
         elif len(args) == 1 and args[0] == ():
-            return SequenceIncompleteValue(tuple, [])  # empty tuple
+            return SequenceValue(tuple, [])  # empty tuple
         else:
-            args_vals = [
-                _type_from_runtime(arg, ctx, unpack_allowed=True) for arg in args
-            ]
-            if any(isinstance(val, UnpackedValue) for val in args_vals):
-                members = []
-                for val in args_vals:
-                    if isinstance(val, UnpackedValue):
-                        members += val.elements
-                    else:
-                        members.append((False, val))
-                return SequenceValue(tuple, members)
-            return SequenceIncompleteValue(tuple, args_vals)
+            return _make_sequence_value(
+                tuple,
+                [_type_from_runtime(arg, ctx, unpack_allowed=True) for arg in args],
+            )
     elif is_instance_of_typing_name(val, "_TypedDictMeta"):
         required_keys = getattr(val, "__required_keys__", None)
         # 3.8's typing.TypedDict doesn't have __required_keys__. With
@@ -799,18 +796,12 @@ def _type_from_subscripted_value(
         if len(members) == 2 and members[1] == KnownValue(Ellipsis):
             return GenericValue(tuple, [_type_from_value(members[0], ctx)])
         elif len(members) == 1 and members[0] == KnownValue(()):
-            return SequenceIncompleteValue(tuple, [])
+            return SequenceValue(tuple, [])
         else:
-            args = [_type_from_value(arg, ctx, unpack_allowed=True) for arg in members]
-            if any(isinstance(val, UnpackedValue) for val in args):
-                tuple_members = []
-                for val in args:
-                    if isinstance(val, UnpackedValue):
-                        tuple_members += val.elements
-                    else:
-                        tuple_members.append((False, val))
-                return SequenceValue(tuple, tuple_members)
-            return SequenceIncompleteValue(tuple, args)
+            return _make_sequence_value(
+                tuple,
+                [_type_from_value(arg, ctx, unpack_allowed=True) for arg in members],
+            )
     elif root is typing.Optional:
         if len(members) != 1:
             ctx.show_error("Optional[] takes only one argument")
@@ -982,9 +973,7 @@ class _Visitor(ast.NodeVisitor):
     def visit_Subscript(self, node: ast.Subscript) -> Value:
         value = self.visit(node.value)
         index = self.visit(node.slice)
-        if isinstance(index, SequenceIncompleteValue):
-            members = index.members
-        elif isinstance(index, SequenceValue):
+        if isinstance(index, SequenceValue):
             members = index.get_member_sequence()
             if members is None:
                 # TODO support unpacking here
@@ -1009,12 +998,12 @@ class _Visitor(ast.NodeVisitor):
         return AnyValue(AnySource.error)
 
     def visit_Tuple(self, node: ast.Tuple) -> Value:
-        elts = [self.visit(elt) for elt in node.elts]
-        return SequenceIncompleteValue(tuple, elts)
+        elts = [(False, self.visit(elt)) for elt in node.elts]
+        return SequenceValue(tuple, elts)
 
     def visit_List(self, node: ast.List) -> Value:
-        elts = [self.visit(elt) for elt in node.elts]
-        return SequenceIncompleteValue(list, elts)
+        elts = [(False, self.visit(elt)) for elt in node.elts]
+        return SequenceValue(list, elts)
 
     def visit_Index(self, node: ast.Index) -> Value:
         # class is unused in 3.9
@@ -1159,12 +1148,12 @@ def _value_of_origin_args(
         elif len(args) == 2 and args[1] is Ellipsis:
             return GenericValue(tuple, [_type_from_runtime(args[0], ctx)])
         elif len(args) == 1 and args[0] == ():
-            return SequenceIncompleteValue(tuple, [])
+            return SequenceValue(tuple, [])
         else:
             args_vals = [
                 _type_from_runtime(arg, ctx, unpack_allowed=True) for arg in args
             ]
-            return SequenceIncompleteValue(tuple, args_vals)
+            return _make_sequence_value(tuple, args_vals)
     elif origin is typing.Union:
         return unite_values(*[_type_from_runtime(arg, ctx) for arg in args])
     elif origin is Callable or origin is typing.Callable:
@@ -1254,11 +1243,19 @@ def _maybe_typed_value(val: Union[type, str]) -> Value:
     return TypedValue(val)
 
 
+def _make_sequence_value(typ: type, members: Sequence[Value]) -> SequenceValue:
+    pairs = []
+    for val in members:
+        if isinstance(val, UnpackedValue):
+            pairs += val.elements
+        else:
+            pairs.append((False, val))
+    return SequenceValue(typ, pairs)
+
+
 def _make_unpacked_value(val: Value, ctx: Context) -> UnpackedValue:
     if isinstance(val, SequenceValue) and val.typ is tuple:
         return UnpackedValue(val.members)
-    elif isinstance(val, SequenceIncompleteValue) and val.typ is tuple:
-        return UnpackedValue([(False, elt) for elt in val.members])
     elif isinstance(val, GenericValue) and val.typ is tuple:
         return UnpackedValue([(True, val.args[0])])
     elif isinstance(val, TypedValue) and val.typ is tuple:
@@ -1277,16 +1274,28 @@ def _make_callable_from_value(
                 [ELLIPSIS_PARAM], return_annotation=return_annotation, is_asynq=is_asynq
             )
         )
-    elif isinstance(args, SequenceIncompleteValue):
-        params = [
-            SigParameter(
-                f"__arg{i}",
-                kind=ParameterKind.POSITIONAL_ONLY,
-                annotation=_type_from_value(arg, ctx),
-            )
-            for i, arg in enumerate(args.members)
-        ]
-        sig = Signature.make(params, return_annotation, is_asynq=is_asynq)
+    elif isinstance(args, SequenceValue):
+        params = []
+        for i, (is_many, arg) in enumerate(args.members):
+            annotation = _type_from_value(arg, ctx)
+            if is_many:
+                param = SigParameter(
+                    f"__arg{i}",
+                    kind=ParameterKind.VAR_POSITIONAL,
+                    annotation=GenericValue(tuple, [annotation]),
+                )
+            else:
+                param = SigParameter(
+                    f"__arg{i}",
+                    kind=ParameterKind.POSITIONAL_ONLY,
+                    annotation=annotation,
+                )
+            params.append(param)
+        try:
+            sig = Signature.make(params, return_annotation, is_asynq=is_asynq)
+        except InvalidSignature as e:
+            ctx.show_error(str(e))
+            return AnyValue(AnySource.error)
         return CallableValue(sig)
     elif isinstance(args, KnownValue) and is_instance_of_typing_name(
         args.val, "ParamSpec"
diff --git a/pyanalyze/boolability.py b/pyanalyze/boolability.py
index 1552f07..90e3ca3 100644
--- a/pyanalyze/boolability.py
+++ b/pyanalyze/boolability.py
@@ -19,7 +19,6 @@ from .value import (
     DictIncompleteValue,
     KnownValue,
     MultiValuedValue,
-    SequenceIncompleteValue,
     SequenceValue,
     SubclassValue,
     TypedDictValue,
@@ -111,21 +110,6 @@ def _get_boolability_no_mvv(value: Value) -> Boolability:
             return Boolability.type_always_true
         else:
             return Boolability.boolable
-    elif isinstance(value, SequenceIncompleteValue):
-        if value.typ is tuple:
-            if value.members:
-                # We lie slightly here, since at the type level a tuple
-                # may be false. But tuples are a common source of boolability
-                # bugs and they're rarely mutated, so we put a stronger
-                # condition on them.
-                return Boolability.type_always_true
-            else:
-                return Boolability.value_always_false
-        else:
-            if value.members:
-                return Boolability.value_always_true_mutable
-            else:
-                return Boolability.value_always_false_mutable
     elif isinstance(value, SequenceValue):
         if not value.members:
             if value.typ is tuple:
diff --git a/pyanalyze/format_strings.py b/pyanalyze/format_strings.py
index 84c9d82..412b89b 100644
--- a/pyanalyze/format_strings.py
+++ b/pyanalyze/format_strings.py
@@ -29,7 +29,6 @@ from .value import (
     CanAssignContext,
     KnownValue,
     DictIncompleteValue,
-    SequenceIncompleteValue,
     SequenceValue,
     TypedValue,
     Value,
@@ -369,9 +368,7 @@ class PercentFormatString:
             if isinstance(args, AnnotatedValue):
                 args = args.value
             args = replace_known_sequence_value(args)
-            if isinstance(args, SequenceIncompleteValue):
-                all_args = args.members
-            elif isinstance(args, SequenceValue):
+            if isinstance(args, SequenceValue):
                 all_args = args.get_member_sequence()
                 if all_args is None:
                     return
diff --git a/pyanalyze/implementation.py b/pyanalyze/implementation.py
index a88ed59..bd1f9cf 100644
--- a/pyanalyze/implementation.py
+++ b/pyanalyze/implementation.py
@@ -42,7 +42,6 @@ from .value import (
     GenericValue,
     NewTypeValue,
     DictIncompleteValue,
-    SequenceIncompleteValue,
     TypedDictValue,
     KnownValue,
     MultiValuedValue,
@@ -346,7 +345,8 @@ def _sequence_impl(typ: type, ctx: CallContext) -> ImplReturn:
         elif isinstance(cvi, Value):
             return GenericValue(typ, [cvi])
         else:
-            return SequenceIncompleteValue.make_or_known(typ, cvi)
+            # TODO: Consider changing concrete_values_from_iterable to preserve unpacked bits
+            return SequenceValue.make_or_known(typ, [(False, elt) for elt in cvi])
 
     return flatten_unions(inner, iterable)
 
@@ -356,15 +356,7 @@ def _list_append_impl(ctx: CallContext) -> ImplReturn:
     element = ctx.vars["object"]
     varname = ctx.visitor.varname_for_self_constraint(ctx.node)
     if varname is not None:
-        if isinstance(lst, SequenceIncompleteValue):
-            no_return_unless = Constraint(
-                varname,
-                ConstraintType.is_value_object,
-                True,
-                SequenceIncompleteValue.make_or_known(list, (*lst.members, element)),
-            )
-            return ImplReturn(KnownValue(None), no_return_unless=no_return_unless)
-        elif isinstance(lst, SequenceValue):
+        if isinstance(lst, SequenceValue):
             no_return_unless = Constraint(
                 varname,
                 ConstraintType.is_value_object,
@@ -436,15 +428,6 @@ def _sequence_getitem_impl(ctx: CallContext, typ: type) -> ImplReturn:
                                     return member
                     # fall back to the common type
                     return self_value.args[0]
-                elif isinstance(self_value, SequenceIncompleteValue):
-                    if -len(self_value.members) <= key.val < len(self_value.members):
-                        return self_value.members[key.val]
-                    elif typ is list:
-                        # fall back to the common type
-                        return self_value.args[0]
-                    else:
-                        ctx.show_error(f"Tuple index out of range: {key}")
-                        return AnyValue(AnySource.error)
                 else:
                     return self_value.get_generic_arg_for_type(typ, ctx.visitor, 0)
             elif isinstance(key.val, slice):
@@ -458,10 +441,6 @@ def _sequence_getitem_impl(ctx: CallContext, typ: type) -> ImplReturn:
                         # If the value contains unpacked values, we don't attempt
                         # to resolve the slice.
                         return GenericValue(typ, self_value.args)
-                elif isinstance(self_value, SequenceIncompleteValue):
-                    return SequenceIncompleteValue.make_or_known(
-                        list, self_value.members[key.val]
-                    )
                 elif self_value.typ in (list, tuple):
                     # For generics of exactly list/tuple, return the self type.
                     return self_value
@@ -922,12 +901,6 @@ def _list_add_impl(ctx: CallContext) -> ImplReturn:
         right = replace_known_sequence_value(right)
         if isinstance(left, SequenceValue) and isinstance(right, SequenceValue):
             return SequenceValue.make_or_known(list, [*left.members, *right.members])
-        elif isinstance(left, SequenceIncompleteValue) and isinstance(
-            right, SequenceIncompleteValue
-        ):
-            return SequenceIncompleteValue.make_or_known(
-                list, [*left.members, *right.members]
-            )
         elif isinstance(left, TypedValue) and isinstance(right, TypedValue):
             left_arg = left.get_generic_arg_for_type(list, ctx.visitor, 0)
             right_arg = right.get_generic_arg_for_type(list, ctx.visitor, 0)
@@ -946,36 +919,8 @@ def _list_extend_or_iadd_impl(
     def inner(lst: Value, iterable: Value) -> ImplReturn:
         cleaned_lst = replace_known_sequence_value(lst)
         iterable = replace_known_sequence_value(iterable)
-        if isinstance(cleaned_lst, SequenceIncompleteValue):
-            if isinstance(
-                iterable, SequenceIncompleteValue
-            ) and iterable.get_type_object(ctx.visitor).is_exactly((list, tuple)):
-                constrained_value = SequenceIncompleteValue.make_or_known(
-                    list, (*cleaned_lst.members, *iterable.members)
-                )
-            else:
-                if isinstance(iterable, TypedValue):
-                    arg_type = iterable.get_generic_arg_for_type(
-                        collections.abc.Iterable, ctx.visitor, 0
-                    )
-                else:
-                    arg_type = AnyValue(AnySource.generic_argument)
-                generic_arg = unite_values(*cleaned_lst.members, arg_type)
-                constrained_value = make_weak(GenericValue(list, [generic_arg]))
-            if return_container:
-                return ImplReturn(constrained_value)
-            if varname is not None:
-                no_return_unless = Constraint(
-                    varname, ConstraintType.is_value_object, True, constrained_value
-                )
-                return ImplReturn(KnownValue(None), no_return_unless=no_return_unless)
-        elif isinstance(cleaned_lst, SequenceValue):
-            if isinstance(iterable, SequenceIncompleteValue):
-                constrained_value = SequenceValue.make_or_known(
-                    list,
-                    (*cleaned_lst.members, *[(False, m) for m in iterable.members]),
-                )
-            elif isinstance(iterable, SequenceValue):
+        if isinstance(cleaned_lst, SequenceValue):
+            if isinstance(iterable, SequenceValue):
                 constrained_value = SequenceValue.make_or_known(
                     list, (*cleaned_lst.members, *iterable.members)
                 )
@@ -1073,17 +1018,7 @@ def _set_add_impl(ctx: CallContext) -> ImplReturn:
     element = ctx.vars["object"]
     varname = ctx.visitor.varname_for_self_constraint(ctx.node)
     if varname is not None:
-        if isinstance(set_value, SequenceIncompleteValue):
-            no_return_unless = Constraint(
-                varname,
-                ConstraintType.is_value_object,
-                True,
-                SequenceIncompleteValue.make_or_known(
-                    set, (*set_value.members, element)
-                ),
-            )
-            return ImplReturn(KnownValue(None), no_return_unless=no_return_unless)
-        elif isinstance(set_value, SequenceValue):
+        if isinstance(set_value, SequenceValue):
             no_return_unless = Constraint(
                 varname,
                 ConstraintType.is_value_object,
@@ -1181,9 +1116,7 @@ def _str_format_impl(ctx: CallContext) -> Value:
     if not isinstance(self, KnownValue):
         return TypedValue(str)
     args_value = replace_known_sequence_value(ctx.vars["args"])
-    if isinstance(args_value, SequenceIncompleteValue):
-        args = args_value.members
-    elif isinstance(args_value, SequenceValue):
+    if isinstance(args_value, SequenceValue):
         args = args_value.get_member_sequence()
         if args is None:
             return TypedValue(str)
@@ -1313,12 +1246,6 @@ def _qcore_assert_impl(
 
 
 def len_of_value(val: Value) -> Value:
-    if (
-        isinstance(val, SequenceIncompleteValue)
-        and isinstance(val.typ, type)
-        and not issubclass(val.typ, KNOWN_MUTABLE_TYPES)
-    ):
-        return KnownValue(len(val.members))
     if (
         isinstance(val, SequenceValue)
         and isinstance(val.typ, type)
diff --git a/pyanalyze/name_check_visitor.py b/pyanalyze/name_check_visitor.py
index 96e0771..d91e287 100644
--- a/pyanalyze/name_check_visitor.py
+++ b/pyanalyze/name_check_visitor.py
@@ -87,13 +87,7 @@ from .patma import PatmaVisitor
 from .predicates import EqualsPredicate
 from .shared_options import Paths, ImportPaths, EnforceNoUnused
 from .reexport import ImplicitReexportTracker
-from .safe import (
-    safe_getattr,
-    is_hashable,
-    all_of_type,
-    safe_issubclass,
-    is_dataclass_type,
-)
+from .safe import safe_getattr, is_hashable, safe_issubclass, is_dataclass_type
 from .stacked_scopes import (
     EMPTY_ORIGIN,
     AbstractConstraint,
@@ -185,7 +179,6 @@ from .value import (
     ReferencingValue,
     SubclassValue,
     DictIncompleteValue,
-    SequenceIncompleteValue,
     AsyncTaskIncompleteValue,
     GenericValue,
     Value,
@@ -2439,10 +2432,10 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
                         for val in iterable_type:
                             self.visit_comprehension(generator, iterable_type=val)
                             with qcore.override(self, "in_comprehension_body", True):
-                                elts.append(self.visit(node.elt))
+                                elts.append((False, self.visit(node.elt)))
                     finally:
                         self.node_context.contexts.pop()
-                    return SequenceIncompleteValue(typ, elts)
+                    return SequenceValue(typ, elts)
 
             iterable_type = unite_and_simplify(
                 *iterable_type,
@@ -2713,7 +2706,6 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
         elt_nodes: Optional[Sequence[ast.AST]] = None,
     ) -> Value:
         values = []
-        has_unknown_value = False
         for i, elt in enumerate(elts):
             if isinstance(elt, _StarredValue):
                 vals = concrete_values_from_iterable(elt.value, self)
@@ -2724,16 +2716,14 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
                         ErrorCode.unsupported_operation,
                         detail=str(vals),
                     )
-                    new_vals = [AnyValue(AnySource.error)]
-                    has_unknown_value = True
+                    new_vals = [(True, AnyValue(AnySource.error))]
                 elif isinstance(vals, Value):
                     # single value
-                    has_unknown_value = True
-                    new_vals = [vals]
+                    new_vals = [(True, vals)]
                 else:
-                    new_vals = vals
+                    new_vals = [(False, val) for val in vals]
                 if typ is set:
-                    for val in new_vals:
+                    for _, val in new_vals:
                         hashability = check_hashability(val, self)
                         if isinstance(hashability, CanAssignError):
                             if elt_nodes:
@@ -2762,22 +2752,9 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
                             ErrorCode.unhashable_key,
                             detail=str(hashability),
                         )
-                values.append(elt)
-        if has_unknown_value:
-            arg = unite_and_simplify(
-                *values, limit=self.options.get_value_for(UnionSimplificationLimit)
-            )
-            return make_weak(GenericValue(typ, [arg]))
-        elif all_of_type(values, KnownValue):
-            vals = [elt.val for elt in values]
-            try:
-                obj = typ(vals)
-            except TypeError:
-                # probably an unhashable type being included in a set
-                return TypedValue(typ)
-            return KnownValue(obj)
-        else:
-            return SequenceIncompleteValue(typ, values)
+                values.append((False, elt))
+
+        return SequenceValue.make_or_known(typ, values)
 
     # Operations
 
@@ -3213,14 +3190,7 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
             # https://github.com/quora/asynq/blob/b07682d8b11e53e4ee5c585020cc9033e239c7eb/asynq/async_task.py#L446
             value.get_type_object().is_exactly({list, tuple})
         ):
-            if isinstance(value, SequenceIncompleteValue) and isinstance(
-                value.typ, type
-            ):
-                values = [
-                    self._unwrap_yield_result(node, member) for member in value.members
-                ]
-                return self._maybe_make_sequence(value.typ, values, node)
-            elif isinstance(value, SequenceValue) and isinstance(value.typ, type):
+            if isinstance(value, SequenceValue) and isinstance(value.typ, type):
                 values = [
                     (is_many, self._unwrap_yield_result(node, member))
                     for is_many, member in value.members
@@ -4016,15 +3986,6 @@ class NameCheckVisitor(node_visitor.ReplacingNodeVisitor):
                     return_value = KnownValue(type[index.val])
                 else:
                     return_value = AnyValue(AnySource.inference)
-            elif (
-                isinstance(value, SequenceIncompleteValue)
-                and isinstance(index, KnownValue)
-                and isinstance(index.val, int)
-                and -len(value.members) <= index.val < len(value.members)
-            ):
-                # Don't error if it's out of range; the object may be mutated at runtime.
-                # TODO: handle slices; error for things that aren't ints or slices.
-                return_value = value.members[index.val]
             else:
                 with self.catch_errors():
                     getitem = self._get_dunder(node.value, value, "__getitem__")
diff --git a/pyanalyze/patma.py b/pyanalyze/patma.py
index ef6a43f..1890559 100644
--- a/pyanalyze/patma.py
+++ b/pyanalyze/patma.py
@@ -50,7 +50,6 @@ from .value import (
     CustomCheckExtension,
     DictIncompleteValue,
     KVPair,
-    SequenceIncompleteValue,
     SequenceValue,
     SubclassValue,
     TypedValue,
@@ -167,8 +166,8 @@ class LenPredicate:
         ):
             # Narrow Tuple[...] to a known length
             arg = cleaned.get_generic_arg_for_type(tuple, self.ctx, 0)
-            return SequenceIncompleteValue(
-                tuple, [arg for _ in range(self.expected_length)]
+            return SequenceValue(
+                tuple, [(False, arg) for _ in range(self.expected_length)]
             )
         return value
 
@@ -479,20 +478,7 @@ def get_match_args(
     if match_args_value is UNINITIALIZED_VALUE:
         return CanAssignError(f"{cls} has no attribute __match_args__")
     match_args_value = replace_known_sequence_value(match_args_value)
-    if isinstance(match_args_value, SequenceIncompleteValue):
-        if match_args_value.typ is not tuple:
-            return CanAssignError(
-                f"__match_args__ must be a literal tuple, not {match_args_value}"
-            )
-        match_args = []
-        for i, arg in enumerate(match_args_value.members):
-            if not isinstance(arg, KnownValue) or not isinstance(arg.val, str):
-                return CanAssignError(
-                    f"__match_args__ element {i} is {arg}, not a string literal"
-                )
-            match_args.append(arg.val)
-        return match_args
-    elif isinstance(match_args_value, SequenceValue):
+    if isinstance(match_args_value, SequenceValue):
         if match_args_value.typ is not tuple:
             return CanAssignError(
                 f"__match_args__ must be a literal tuple, not {match_args_value}"
diff --git a/pyanalyze/signature.py b/pyanalyze/signature.py
index c5e4bcc..d27c4fe 100644
--- a/pyanalyze/signature.py
+++ b/pyanalyze/signature.py
@@ -42,7 +42,6 @@ from .value import (
     ParamSpecArgsValue,
     ParamSpecKwargsValue,
     ParameterTypeGuardExtension,
-    SequenceIncompleteValue,
     DictIncompleteValue,
     SequenceValue,
     TypeGuardExtension,
@@ -924,10 +923,17 @@ class Signature:
                         tuple, [AnyValue(AnySource.ellipsis_callable)]
                     )
                 elif actual_args.star_args is not None:
-                    element_value = unite_values(*positionals, actual_args.star_args)
-                    star_args_value = GenericValue(tuple, [element_value])
+                    star_args_value = SequenceValue(
+                        tuple,
+                        [
+                            *[(False, pos) for pos in positionals],
+                            (True, actual_args.star_args),
+                        ],
+                    )
                 else:
-                    star_args_value = SequenceIncompleteValue(tuple, positionals)
+                    star_args_value = SequenceValue(
+                        tuple, [(False, pos) for pos in positionals]
+                    )
                     if not positionals:
                         # no *args were actually provided
                         position = DEFAULT
@@ -2350,7 +2356,6 @@ MappingValue = GenericValue(collections.abc.Mapping, [TypeVarValue(K), TypeVarVa
 def can_assign_var_positional(
     my_param: SigParameter, args_annotation: Value, idx: int, ctx: CanAssignContext
 ) -> Union[List[BoundsMap], CanAssignError]:
-    bounds_maps = []
     my_annotation = my_param.get_annotation()
     if isinstance(args_annotation, SequenceValue):
         members = args_annotation.get_member_sequence()
@@ -2369,41 +2374,20 @@ def can_assign_var_positional(
                     " type is incompatible",
                     [can_assign],
                 )
-            bounds_maps.append(can_assign)
-            return bounds_maps
-
-    if isinstance(args_annotation, SequenceIncompleteValue):
-        length = len(args_annotation.members)
-        if idx >= length:
-            return CanAssignError(
-                f"parameter {my_param.name!r} is not accepted; {args_annotation} only"
-                f" accepts {length} values"
-            )
-        their_annotation = args_annotation.members[idx]
-        can_assign = their_annotation.can_assign(my_annotation, ctx)
-        if isinstance(can_assign, CanAssignError):
-            return CanAssignError(
-                f"type of parameter {my_param.name!r} is incompatible: *args[{idx}]"
-                " type is incompatible",
-                [can_assign],
-            )
-        bounds_maps.append(can_assign)
-    else:
-        tv_map = get_tv_map(IterableValue, args_annotation, ctx)
-        if isinstance(tv_map, CanAssignError):
-            return CanAssignError(
-                f"{args_annotation} is not an iterable type", [tv_map]
-            )
-        iterable_arg = tv_map.get(T, AnyValue(AnySource.generic_argument))
-        bounds_map = iterable_arg.can_assign(my_annotation, ctx)
-        if isinstance(bounds_map, CanAssignError):
-            return CanAssignError(
-                f"type of parameter {my_param.name!r} is incompatible: "
-                "*args type is incompatible",
-                [bounds_map],
-            )
-        bounds_maps.append(bounds_map)
-    return bounds_maps
+            return [can_assign]
+
+    tv_map = get_tv_map(IterableValue, args_annotation, ctx)
+    if isinstance(tv_map, CanAssignError):
+        return CanAssignError(f"{args_annotation} is not an iterable type", [tv_map])
+    iterable_arg = tv_map.get(T, AnyValue(AnySource.generic_argument))
+    bounds_map = iterable_arg.can_assign(my_annotation, ctx)
+    if isinstance(bounds_map, CanAssignError):
+        return CanAssignError(
+            f"type of parameter {my_param.name!r} is incompatible: "
+            "*args type is incompatible",
+            [bounds_map],
+        )
+    return [bounds_map]
 
 
 def can_assign_var_keyword(
diff --git a/pyanalyze/suggested_type.py b/pyanalyze/suggested_type.py
index 35a7216..e3eced3 100644
--- a/pyanalyze/suggested_type.py
+++ b/pyanalyze/suggested_type.py
@@ -21,7 +21,6 @@ from .value import (
     CanAssignError,
     GenericValue,
     KnownValue,
-    SequenceIncompleteValue,
     SequenceValue,
     SubclassValue,
     TypedDictValue,
@@ -156,13 +155,6 @@ def prepare_type(value: Value) -> Value:
     """Simplify a type to turn it into a suggestion."""
     if isinstance(value, AnnotatedValue):
         return prepare_type(value.value)
-    elif isinstance(value, SequenceIncompleteValue):
-        if value.typ is tuple:
-            return SequenceIncompleteValue(
-                tuple, [prepare_type(elt) for elt in value.members]
-            )
-        else:
-            return GenericValue(value.typ, [prepare_type(arg) for arg in value.args])
     elif isinstance(value, SequenceValue):
         if value.typ is tuple:
             members = value.get_member_sequence()
diff --git a/pyanalyze/type_evaluation.py b/pyanalyze/type_evaluation.py
index 5461c0c..6fc698b 100644
--- a/pyanalyze/type_evaluation.py
+++ b/pyanalyze/type_evaluation.py
@@ -33,7 +33,6 @@ from .stacked_scopes import (
     VarnameWithOrigin,
     constrain_value,
 )
-from .safe import all_of_type
 from .value import (
     NO_RETURN_VALUE,
     BoundsMap,
@@ -42,7 +41,6 @@ from .value import (
     CanAssignError,
     KnownValue,
     MultiValuedValue,
-    SequenceIncompleteValue,
     SequenceValue,
     Value,
     flatten_values,
@@ -602,12 +600,6 @@ class ConditionEvaluator(ast.NodeVisitor):
 
     def evaluate_literal(self, node: ast.expr) -> Optional[KnownValue]:
         val = self.evaluator.evaluate_value(node)
-        if (
-            isinstance(val, SequenceIncompleteValue)
-            and isinstance(val.typ, type)
-            and all_of_type(val.members, KnownValue)
-        ):
-            val = KnownValue(val.typ(elt.val for elt in val.members))
         if isinstance(val, SequenceValue):
             val = val.make_known_value()
         if isinstance(val, KnownValue):
diff --git a/pyanalyze/value.py b/pyanalyze/value.py
index fc8c993..775589a 100644
--- a/pyanalyze/value.py
+++ b/pyanalyze/value.py
@@ -51,6 +51,7 @@ from typing_extensions import Literal, Protocol, ParamSpec
 import pyanalyze
 from pyanalyze.extensions import CustomCheck
 
+from .find_unused import used
 from .safe import all_of_type, safe_equals, safe_issubclass, safe_isinstance
 
 T = TypeVar("T")
@@ -848,7 +849,7 @@ class GenericValue(TypedValue):
                     can_assign = expected.can_assign(value, ctx)
                     if isinstance(can_assign, CanAssignError):
                         return CanAssignError(f"In TypedDict key {key!r}", [can_assign])
-        elif isinstance(other, SequenceIncompleteValue) and self.typ in {
+        elif isinstance(other, SequenceValue) and self.typ in {
             list,
             set,
             tuple,
@@ -858,7 +859,7 @@ class GenericValue(TypedValue):
             collections.abc.Container,
             collections.abc.Collection,
         }:
-            for i, key in enumerate(other.members):
+            for i, (_, key) in enumerate(other.members):
                 can_assign = expected.can_assign(key, ctx)
                 if isinstance(can_assign, CanAssignError):
                     return CanAssignError(f"In element {i}", [can_assign])
@@ -938,43 +939,14 @@ class SequenceValue(GenericValue):
             if is_many or not isinstance(member, KnownValue):
                 return SequenceValue(typ, members)
             known_members.append(member.val)
-        return KnownValue(typ(known_members))
+        try:
+            return KnownValue(typ(known_members))
+        except TypeError:
+            # Probably an unhashable object in a set.
+            return SequenceValue(typ, members)
 
     def can_assign(self, other: Value, ctx: CanAssignContext) -> CanAssign:
-        if isinstance(other, SequenceIncompleteValue):
-            can_assign = self.get_type_object(ctx).can_assign(self, other, ctx)
-            if isinstance(can_assign, CanAssignError):
-                return CanAssignError(
-                    f"Cannot assign {stringify_object(other.typ)} to"
-                    f" {stringify_object(self.typ)}"
-                )
-            my_len = len(self.members)
-            their_len = len(other.members)
-            if my_len != their_len:
-                type_str = stringify_object(self.typ)
-                return CanAssignError(
-                    f"Cannot assign {type_str} of length {their_len} to {type_str} of"
-                    f" length {my_len}"
-                )
-            if my_len == 0:
-                return {}  # they're both empty
-            bounds_maps = [can_assign]
-            for i, ((is_many, my_member), their_member) in enumerate(
-                zip(self.members, other.members)
-            ):
-                if is_many:
-                    return CanAssignError(
-                        f"Member {i} is an unpacked type, but a non-unpacked type is"
-                        " provided"
-                    )
-                can_assign = my_member.can_assign(their_member, ctx)
-                if isinstance(can_assign, CanAssignError):
-                    return CanAssignError(
-                        f"Types for member {i} are incompatible", [can_assign]
-                    )
-                bounds_maps.append(can_assign)
-            return unify_bounds_maps(bounds_maps)
-        elif isinstance(other, SequenceValue):
+        if isinstance(other, SequenceValue):
             can_assign = self.get_type_object(ctx).can_assign(self, other, ctx)
             if isinstance(can_assign, CanAssignError):
                 return CanAssignError(
@@ -1052,6 +1024,7 @@ class SequenceValue(GenericValue):
 
 
 # TODO(jelle): Replace with SequenceValue
+@used  # for compatibility for now
 @dataclass(unsafe_hash=True, init=False)
 class SequenceIncompleteValue(GenericValue):
     """A :class:`TypedValue` subclass representing a sequence of known type and length.
@@ -2429,8 +2402,6 @@ def concrete_values_from_iterable(
         if not value_subvals and len(set(map(len, seq_subvals))) == 1:
             return [unite_values(*vals) for vals in zip(*seq_subvals)]
         return unite_values(*value_subvals, *chain.from_iterable(seq_subvals))
-    if isinstance(value, SequenceIncompleteValue):
-        return value.members
     if isinstance(value, SequenceValue):
         members = value.get_member_sequence()
         if members is None:
@@ -2631,18 +2602,7 @@ def unpack_values(
     #   iterable approach. We experimented both with treating lists
     #   like tuples and with always falling back, and both approaches
     #   led to false positives.
-    if isinstance(value, SequenceIncompleteValue):
-        if value.typ is tuple:
-            return _unpack_value_sequence(
-                value, value.members, target_length, post_starred_length
-            )
-        elif value.typ is list:
-            vals = _unpack_value_sequence(
-                value, value.members, target_length, post_starred_length
-            )
-            if not isinstance(vals, CanAssignError):
-                return vals
-    elif isinstance(value, SequenceValue):
+    if isinstance(value, SequenceValue):
         if value.typ is tuple:
             return _unpack_sequence_value(value, target_length, post_starred_length)
         elif value.typ is list:
@@ -2710,7 +2670,7 @@ def _unpack_sequence_value(
             return CanAssignError(f"{value} must have exactly {target_length} elements")
         middle_length = remaining_target_length - len(tail)
         fallback_value = unite_values(*[val for _, val in remaining_members])
-        return [*head, *[fallback_value for _ in range(middle_length)], *tail]
+        return [*head, *[fallback_value for _ in range(middle_length)], *reversed(tail)]
     else:
         while len(tail) < post_starred_length:
             if len(tail) >= len(value.members) - len(head):
@@ -2741,40 +2701,10 @@ def _unpack_sequence_value(
                     *[fallback_value for _ in range(remaining_target_length)],
                     GenericValue(list, [fallback_value]),
                     *[fallback_value for _ in range(remaining_post_starred_length)],
-                    *tail,
+                    *reversed(tail),
                 ]
         else:
-            return [*head, SequenceValue(list, remaining_members), *tail]
-
-
-def _unpack_value_sequence(
-    value: Value,
-    members: Sequence[Value],
-    target_length: int,
-    post_starred_length: Optional[int],
-) -> Union[Sequence[Value], CanAssignError]:
-    actual_length = len(members)
-    if post_starred_length is None:
-        if actual_length != target_length:
-            return CanAssignError(
-                f"{value} is of length {actual_length} (expected {target_length})"
-            )
-        return members
-    if actual_length < target_length + post_starred_length:
-        return CanAssignError(
-            f"{value} is of length {actual_length} (expected at least"
-            f" {target_length + post_starred_length})"
-        )
-    head = members[:target_length]
-    if post_starred_length > 0:
-        body = SequenceIncompleteValue(
-            list, members[target_length:-post_starred_length]
-        )
-        tail = members[-post_starred_length:]
-    else:
-        body = SequenceIncompleteValue(list, members[target_length:])
-        tail = []
-    return [*head, body, *tail]
+            return [*head, SequenceValue(list, remaining_members), *reversed(tail)]
 
 
 def replace_known_sequence_value(value: Value) -> Value:
@@ -2785,7 +2715,7 @@ def replace_known_sequence_value(value: Value) -> Value:
     - Replace AnnotatedValue with its inner type
     - Replace TypeVarValue with its fallback type
     - Replace KnownValues representing list, tuples, sets, or dicts with
-      SequenceIncompleteValue or DictIncompleteValue.
+      SequenceValue or DictIncompleteValue.
 
     """
     if isinstance(value, AnnotatedValue):
@@ -2794,8 +2724,8 @@ def replace_known_sequence_value(value: Value) -> Value:
         return replace_known_sequence_value(value.get_fallback_value())
     if isinstance(value, KnownValue):
         if isinstance(value.val, (list, tuple, set)):
-            return SequenceIncompleteValue(
-                type(value.val), [KnownValue(elt) for elt in value.val]
+            return SequenceValue(
+                type(value.val), [(False, KnownValue(elt)) for elt in value.val]
             )
         elif isinstance(value.val, dict):
             return DictIncompleteValue(


Test patch:
diff --git a/pyanalyze/test.toml b/pyanalyze/test.toml
index 1c60e58..a906a1f 100644
--- a/pyanalyze/test.toml
+++ b/pyanalyze/test.toml
@@ -6,3 +6,4 @@ constructor_hooks = ["pyanalyze.test_config.get_constructor"]
 known_signatures = ["pyanalyze.test_config.get_known_signatures"]
 unwrap_class = ["pyanalyze.test_config.unwrap_class"]
 stub_path = ["./stubs"]
+functions_safe_to_call = ["pyanalyze.tests.make_simple_sequence"]
diff --git a/pyanalyze/test_annotations.py b/pyanalyze/test_annotations.py
index 7d20736..118fd49 100644
--- a/pyanalyze/test_annotations.py
+++ b/pyanalyze/test_annotations.py
@@ -11,7 +11,6 @@ from .value import (
     KnownValue,
     MultiValuedValue,
     NewTypeValue,
-    SequenceIncompleteValue,
     SequenceValue,
     TypeVarValue,
     TypedDictValue,
@@ -19,6 +18,7 @@ from .value import (
     SubclassValue,
     GenericValue,
 )
+from .tests import make_simple_sequence
 
 
 class TestAnnotations(TestNameCheckVisitorBase):
@@ -338,22 +338,11 @@ class TestAnnotations(TestNameCheckVisitorBase):
             empty: Tuple[()],
         ) -> None:
             assert_is_value(x, GenericValue(tuple, [TypedValue(int)]))
-            assert_is_value(y, SequenceIncompleteValue(tuple, [TypedValue(int)]))
-            assert_is_value(
-                z, SequenceIncompleteValue(tuple, [TypedValue(str), TypedValue(int)])
-            )
-            assert_is_value(
-                omega,
-                MultiValuedValue(
-                    [
-                        SequenceIncompleteValue(
-                            tuple, [TypedValue(str), TypedValue(int)]
-                        ),
-                        KnownValue(None),
-                    ]
-                ),
-            )
-            assert_is_value(empty, SequenceIncompleteValue(tuple, []))
+            assert_is_value(y, make_simple_sequence(tuple, [TypedValue(int)]))
+            t_str_int = make_simple_sequence(tuple, [TypedValue(str), TypedValue(int)])
+            assert_is_value(z, t_str_int)
+            assert_is_value(omega, t_str_int | KnownValue(None))
+            assert_is_value(empty, SequenceValue(tuple, []))
 
     @assert_passes()
     def test_stringified_tuples(self):
@@ -367,22 +356,11 @@ class TestAnnotations(TestNameCheckVisitorBase):
             empty: "Tuple[()]",
         ) -> None:
             assert_is_value(x, GenericValue(tuple, [TypedValue(int)]))
-            assert_is_value(y, SequenceIncompleteValue(tuple, [TypedValue(int)]))
-            assert_is_value(
-                z, SequenceIncompleteValue(tuple, [TypedValue(str), TypedValue(int)])
-            )
-            assert_is_value(
-                omega,
-                MultiValuedValue(
-                    [
-                        SequenceIncompleteValue(
-                            tuple, [TypedValue(str), TypedValue(int)]
-                        ),
-                        KnownValue(None),
-                    ]
-                ),
-            )
-            assert_is_value(empty, SequenceIncompleteValue(tuple, []))
+            assert_is_value(y, make_simple_sequence(tuple, [TypedValue(int)]))
+            t_str_int = make_simple_sequence(tuple, [TypedValue(str), TypedValue(int)])
+            assert_is_value(z, t_str_int)
+            assert_is_value(omega, t_str_int | KnownValue(None))
+            assert_is_value(empty, SequenceValue(tuple, []))
 
     @skip_before((3, 9))
     @assert_passes()
@@ -397,22 +375,11 @@ class TestAnnotations(TestNameCheckVisitorBase):
             empty: tuple[()],
         ) -> None:
             assert_is_value(x, GenericValue(tuple, [TypedValue(int)]))
-            assert_is_value(y, SequenceIncompleteValue(tuple, [TypedValue(int)]))
-            assert_is_value(
-                z, SequenceIncompleteValue(tuple, [TypedValue(str), TypedValue(int)])
-            )
-            assert_is_value(
-                omega,
-                MultiValuedValue(
-                    [
-                        SequenceIncompleteValue(
-                            tuple, [TypedValue(str), TypedValue(int)]
-                        ),
-                        KnownValue(None),
-                    ]
-                ),
-            )
-            assert_is_value(empty, SequenceIncompleteValue(tuple, []))
+            assert_is_value(y, make_simple_sequence(tuple, [TypedValue(int)]))
+            t_str_int = make_simple_sequence(tuple, [TypedValue(str), TypedValue(int)])
+            assert_is_value(z, t_str_int)
+            assert_is_value(omega, t_str_int | KnownValue(None))
+            assert_is_value(empty, SequenceValue(tuple, []))
 
     @assert_passes()
     def test_invalid_annotation(self):
@@ -532,7 +499,7 @@ class TestAnnotations(TestNameCheckVisitorBase):
         def capybara(x: list[int], y: tuple[int, str], z: tuple[int, ...]) -> None:
             assert_is_value(x, GenericValue(list, [TypedValue(int)]))
             assert_is_value(
-                y, SequenceIncompleteValue(tuple, [TypedValue(int), TypedValue(str)])
+                y, make_simple_sequence(tuple, [TypedValue(int), TypedValue(str)])
             )
             assert_is_value(z, GenericValue(tuple, [TypedValue(int)]))
 
diff --git a/pyanalyze/test_async_await.py b/pyanalyze/test_async_await.py
index 7a6017e..1595910 100644
--- a/pyanalyze/test_async_await.py
+++ b/pyanalyze/test_async_await.py
@@ -1,13 +1,6 @@
 # static analysis: ignore
-from .value import (
-    GenericValue,
-    KnownValue,
-    TypedValue,
-    make_weak,
-    AnyValue,
-    AnySource,
-    SequenceIncompleteValue,
-)
+from .tests import make_simple_sequence
+from .value import GenericValue, KnownValue, TypedValue, make_weak, AnyValue, AnySource
 from .implementation import assert_is_value
 from .test_node_visitor import assert_passes, only_before
 from .test_name_check_visitor import TestNameCheckVisitorBase
@@ -251,7 +244,7 @@ class TestArgSpec(TestNameCheckVisitorBase):
                 GenericValue(
                     Awaitable,
                     [
-                        SequenceIncompleteValue(
+                        make_simple_sequence(
                             tuple, [TypedValue(StreamReader), TypedValue(StreamWriter)]
                         )
                     ],
diff --git a/pyanalyze/test_asynq.py b/pyanalyze/test_asynq.py
index 6f63ee9..6dd8367 100644
--- a/pyanalyze/test_asynq.py
+++ b/pyanalyze/test_asynq.py
@@ -1,4 +1,5 @@
 # static analysis: ignore
+from .tests import make_simple_sequence
 from .implementation import assert_is_value
 from .value import (
     AnySource,
@@ -9,7 +10,6 @@ from .value import (
     TypedValue,
     GenericValue,
     DictIncompleteValue,
-    SequenceIncompleteValue,
     KVPair,
 )
 from .test_name_check_visitor import TestNameCheckVisitorBase
@@ -75,7 +75,7 @@ class TestUnwrapYield(TestNameCheckVisitorBase):
             vals1 = yield [square.asynq(1), square.asynq(2), square.asynq(3)]
             assert_is_value(
                 vals1,
-                SequenceIncompleteValue(
+                make_simple_sequence(
                     list, [TypedValue(int), TypedValue(int), TypedValue(int)]
                 ),
             )
diff --git a/pyanalyze/test_boolability.py b/pyanalyze/test_boolability.py
index bcb4d2b..cfe18b4 100644
--- a/pyanalyze/test_boolability.py
+++ b/pyanalyze/test_boolability.py
@@ -11,7 +11,6 @@ from .value import (
     DictIncompleteValue,
     KVPair,
     KnownValue,
-    SequenceIncompleteValue,
     SequenceValue,
     TypedDictValue,
     UnboundMethodValue,
@@ -50,18 +49,6 @@ def test_get_boolability() -> None:
     assert Boolability.boolable == get_boolability(
         TypedDictValue({"a": (False, TypedValue(int))})
     )
-    assert Boolability.type_always_true == get_boolability(
-        SequenceIncompleteValue(tuple, [KnownValue(1)])
-    )
-    assert Boolability.value_always_false == get_boolability(
-        SequenceIncompleteValue(tuple, [])
-    )
-    assert Boolability.value_always_true_mutable == get_boolability(
-        SequenceIncompleteValue(list, [KnownValue(1)])
-    )
-    assert Boolability.value_always_false_mutable == get_boolability(
-        SequenceIncompleteValue(list, [])
-    )
     assert Boolability.type_always_true == get_boolability(
         SequenceValue(tuple, [(False, KnownValue(1))])
     )
diff --git a/pyanalyze/test_format_strings.py b/pyanalyze/test_format_strings.py
index 42e36dc..3fed165 100644
--- a/pyanalyze/test_format_strings.py
+++ b/pyanalyze/test_format_strings.py
@@ -16,12 +16,12 @@ from .value import (
     AnyValue,
     KnownValue,
     DictIncompleteValue,
-    SequenceIncompleteValue,
     TypedValue,
 )
 from .test_node_visitor import assert_passes
 from .test_name_check_visitor import TestNameCheckVisitorBase
 from .test_value import CTX
+from .tests import make_simple_sequence
 
 
 PERCENT_TESTCASES = [
@@ -328,12 +328,12 @@ class TestAccept(object):
         )
         self.assert_errors(
             PercentFormatString.from_pattern("%s%s"),
-            SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]),
+            make_simple_sequence(list, [KnownValue(1), KnownValue(2)]),
             [too_few],
         )
         self.assert_errors(
             PercentFormatString.from_pattern("%s%s"),
-            SequenceIncompleteValue(tuple, [KnownValue(1)]),
+            make_simple_sequence(tuple, [KnownValue(1)]),
             [too_few],
         )
 
@@ -343,7 +343,7 @@ class TestAccept(object):
         )
         self.assert_errors(
             PercentFormatString.from_pattern("%s"),
-            SequenceIncompleteValue(tuple, [KnownValue(1), KnownValue(2)]),
+            make_simple_sequence(tuple, [KnownValue(1), KnownValue(2)]),
             [too_many],
         )
 
@@ -355,12 +355,12 @@ class TestAccept(object):
         self.assert_errors(PercentFormatString.from_pattern("%s%%"), KnownValue(1), [])
         self.assert_errors(
             PercentFormatString.from_pattern("%s"),
-            SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]),
+            make_simple_sequence(list, [KnownValue(1), KnownValue(2)]),
             [],
         )
         self.assert_errors(
             PercentFormatString.from_pattern("%s"),
-            SequenceIncompleteValue(tuple, [KnownValue(1)]),
+            make_simple_sequence(tuple, [KnownValue(1)]),
             [],
         )
 
diff --git a/pyanalyze/test_implementation.py b/pyanalyze/test_implementation.py
index 0a6a7eb..b4f5f74 100644
--- a/pyanalyze/test_implementation.py
+++ b/pyanalyze/test_implementation.py
@@ -1,17 +1,17 @@
 # static analysis: ignore
 from .test_name_check_visitor import TestNameCheckVisitorBase
 from .test_node_visitor import assert_passes
-
+from .tests import make_simple_sequence
 from .value import (
     NO_RETURN_VALUE,
     AnnotatedValue,
     AnySource,
     AnyValue,
     KVPair,
+    SequenceValue,
     assert_is_value,
     CallableValue,
     GenericValue,
-    SequenceIncompleteValue,
     KnownValue,
     TypedValue,
     DictIncompleteValue,
@@ -197,9 +197,9 @@ class TestSequenceImpl(TestNameCheckVisitorBase):
             assert_is_value(tuple(i for i in ints), GenericValue(tuple, [one_two]))
             assert_is_value(tuple({i: i for i in ints}), GenericValue(tuple, [one_two]))
 
-            # SequenceIncompleteValue
+            # SequenceValue
             assert_is_value(
-                tuple([int(x)]), SequenceIncompleteValue(tuple, [TypedValue(int)])
+                tuple([int(x)]), make_simple_sequence(tuple, [TypedValue(int)])
             )
 
             # fallback
@@ -389,10 +389,10 @@ class TestGenericMutators(TestNameCheckVisitorBase):
 
         def capybara(x: int):
             lst = [x]
-            assert_is_value(lst, SequenceIncompleteValue(list, [TypedValue(int)]))
+            assert_is_value(lst, make_simple_sequence(list, [TypedValue(int)]))
             lst.append(1)
             assert_is_value(
-                lst, SequenceIncompleteValue(list, [TypedValue(int), KnownValue(1)])
+                lst, make_simple_sequence(list, [TypedValue(int), KnownValue(1)])
             )
 
             lst2: List[str] = ["x"]
@@ -427,10 +427,10 @@ class TestGenericMutators(TestNameCheckVisitorBase):
 
         def capybara(x: int):
             s = {x}
-            assert_is_value(s, SequenceIncompleteValue(set, [TypedValue(int)]))
+            assert_is_value(s, make_simple_sequence(set, [TypedValue(int)]))
             s.add(1)
             assert_is_value(
-                s, SequenceIncompleteValue(set, [TypedValue(int), KnownValue(1)])
+                s, make_simple_sequence(set, [TypedValue(int), KnownValue(1)])
             )
 
             s2: Set[str] = {"x"}
@@ -445,11 +445,10 @@ class TestGenericMutators(TestNameCheckVisitorBase):
         def capybara(x: int, y: str) -> None:
             assert_is_value(
                 [x] + [y],
-                SequenceIncompleteValue(list, [TypedValue(int), TypedValue(str)]),
+                make_simple_sequence(list, [TypedValue(int), TypedValue(str)]),
             )
             assert_is_value(
-                [x] + [1],
-                SequenceIncompleteValue(list, [TypedValue(int), KnownValue(1)]),
+                [x] + [1], make_simple_sequence(list, [TypedValue(int), KnownValue(1)])
             )
             left: List[int] = []
             right: List[str] = []
@@ -487,27 +486,16 @@ class TestGenericMutators(TestNameCheckVisitorBase):
 
         def capybara(x: int, y: str) -> None:
             lst = [x]
-            assert_is_value(lst, SequenceIncompleteValue(list, [TypedValue(int)]))
+            assert_is_value(lst, make_simple_sequence(list, [TypedValue(int)]))
             lst.extend([y])
             assert_is_value(
-                lst, SequenceIncompleteValue(list, [TypedValue(int), TypedValue(str)])
+                lst, make_simple_sequence(list, [TypedValue(int), TypedValue(str)])
             )
-            # If we extend with a set, don't use a SequenceIncompleteValue any more,
-            # because we don't know how many values were added or in what order.
-            # (Technically we do know for a one-element set, but that doesn't seem worth
-            # writing a special case for.)
             lst.extend({float(1.0)})
             assert_is_value(
                 lst,
-                make_weak(
-                    GenericValue(
-                        list,
-                        [
-                            MultiValuedValue(
-                                [TypedValue(int), TypedValue(str), TypedValue(float)]
-                            )
-                        ],
-                    )
+                make_simple_sequence(
+                    list, [TypedValue(int), TypedValue(str), TypedValue(float)]
                 ),
             )
 
@@ -522,27 +510,16 @@ class TestGenericMutators(TestNameCheckVisitorBase):
 
         def capybara(x: int, y: str) -> None:
             lst = [x]
-            assert_is_value(lst, SequenceIncompleteValue(list, [TypedValue(int)]))
+            assert_is_value(lst, make_simple_sequence(list, [TypedValue(int)]))
             lst += [y]
             assert_is_value(
-                lst, SequenceIncompleteValue(list, [TypedValue(int), TypedValue(str)])
+                lst, make_simple_sequence(list, [TypedValue(int), TypedValue(str)])
             )
-            # If we extend with a set, don't use a SequenceIncompleteValue any more,
-            # because we don't know how many values were added or in what order.
-            # (Technically we do know for a one-element set, but that doesn't seem worth
-            # writing a special case for.)
             lst += {float(1.0)}
             assert_is_value(
                 lst,
-                make_weak(
-                    GenericValue(
-                        list,
-                        [
-                            MultiValuedValue(
-                                [TypedValue(int), TypedValue(str), TypedValue(float)]
-                            )
-                        ],
-                    )
+                make_simple_sequence(
+                    list, [TypedValue(int), TypedValue(str), TypedValue(float)]
                 ),
             )
 
@@ -577,61 +554,40 @@ class TestGenericMutators(TestNameCheckVisitorBase):
             lst.extend(func())
             assert_is_value(
                 lst,
-                make_weak(
-                    GenericValue(
-                        list,
-                        [
-                            MultiValuedValue(
-                                [
-                                    KnownValue("a"),
-                                    KnownValue("b"),
-                                    KnownValue("c"),
-                                    KnownValue("d"),
-                                ]
-                            )
-                        ],
-                    )
+                SequenceValue(
+                    list,
+                    [
+                        (False, KnownValue("a")),
+                        (False, KnownValue("b")),
+                        (True, KnownValue("c") | KnownValue("d")),
+                    ],
                 ),
             )
             lst.extend(["e"])
             assert_is_value(
                 lst,
-                make_weak(
-                    GenericValue(
-                        list,
-                        [
-                            MultiValuedValue(
-                                [
-                                    KnownValue("a"),
-                                    KnownValue("b"),
-                                    KnownValue("c"),
-                                    KnownValue("d"),
-                                    KnownValue("e"),
-                                ]
-                            )
-                        ],
-                    )
+                SequenceValue(
+                    list,
+                    [
+                        (False, KnownValue("a")),
+                        (False, KnownValue("b")),
+                        (True, KnownValue("c") | KnownValue("d")),
+                        (False, KnownValue("e")),
+                    ],
                 ),
             )
             lst.append("f")
             assert_is_value(
                 lst,
-                make_weak(
-                    GenericValue(
-                        list,
-                        [
-                            MultiValuedValue(
-                                [
-                                    KnownValue("a"),
-                                    KnownValue("b"),
-                                    KnownValue("c"),
-                                    KnownValue("d"),
-                                    KnownValue("e"),
-                                    KnownValue("f"),
-                                ]
-                            )
-                        ],
-                    )
+                SequenceValue(
+                    list,
+                    [
+                        (False, KnownValue("a")),
+                        (False, KnownValue("b")),
+                        (True, KnownValue("c") | KnownValue("d")),
+                        (False, KnownValue("e")),
+                        (False, KnownValue("f")),
+                    ],
                 ),
             )
 
@@ -645,24 +601,20 @@ class TestGenericMutators(TestNameCheckVisitorBase):
             lst2 = [*lst1, "b"]
             assert_is_value(
                 lst2,
-                make_weak(
-                    GenericValue(
-                        list, [MultiValuedValue([KnownValue("a"), KnownValue("b")])]
-                    )
+                SequenceValue(
+                    list, [(True, KnownValue("a")), (False, KnownValue("b"))]
                 ),
             )
             lst2.append("c")
             assert_is_value(
                 lst2,
-                make_weak(
-                    GenericValue(
-                        list,
-                        [
-                            MultiValuedValue(
-                                [KnownValue("a"), KnownValue("b"), KnownValue("c")]
-                            )
-                        ],
-                    )
+                SequenceValue(
+                    list,
+                    [
+                        (True, KnownValue("a")),
+                        (False, KnownValue("b")),
+                        (False, KnownValue("c")),
+                    ],
                 ),
             )
 
@@ -902,7 +854,7 @@ class TestSequenceGetItem(TestNameCheckVisitorBase):
             assert_is_value(empty[0], AnyValue(AnySource.unreachable))
             assert_is_value(empty[1:], KnownValue([]))
             assert_is_value(empty[i], AnyValue(AnySource.unreachable))
-            assert_is_value(empty[s], SequenceIncompleteValue(list, []))
+            assert_is_value(empty[s], SequenceValue(list, []))
             assert_is_value(empty[unannotated], AnyValue(AnySource.from_another))
 
             known = [1, 2]
@@ -913,7 +865,7 @@ class TestSequenceGetItem(TestNameCheckVisitorBase):
             assert_is_value(known[::-1], KnownValue([2, 1]))
             assert_is_value(known[i], KnownValue(1) | KnownValue(2))
             assert_is_value(
-                known[s], SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)])
+                known[s], make_simple_sequence(list, [KnownValue(1), KnownValue(2)])
             )
             assert_is_value(known[unannotated], AnyValue(AnySource.from_another))
 
@@ -934,7 +886,7 @@ class TestSequenceGetItem(TestNameCheckVisitorBase):
             assert_is_value(empty[0], AnyValue(AnySource.error))  # E: incompatible_call
             assert_is_value(empty[1:], KnownValue(()))
             assert_is_value(empty[i], AnyValue(AnySource.unreachable))
-            assert_is_value(empty[s], SequenceIncompleteValue(tuple, []))
+            assert_is_value(empty[s], SequenceValue(tuple, []))
             assert_is_value(empty[unannotated], AnyValue(AnySource.from_another))
 
             known = (1, 2)
@@ -947,7 +899,7 @@ class TestSequenceGetItem(TestNameCheckVisitorBase):
             assert_is_value(known[::-1], KnownValue((2, 1)))
             assert_is_value(known[i], KnownValue(1) | KnownValue(2))
             assert_is_value(
-                known[s], SequenceIncompleteValue(tuple, [KnownValue(1), KnownValue(2)])
+                known[s], make_simple_sequence(tuple, [KnownValue(1), KnownValue(2)])
             )
             assert_is_value(known[unannotated], AnyValue(AnySource.from_another))
 
diff --git a/pyanalyze/test_name_check_visitor.py b/pyanalyze/test_name_check_visitor.py
index 73e72b9..096f9ef 100644
--- a/pyanalyze/test_name_check_visitor.py
+++ b/pyanalyze/test_name_check_visitor.py
@@ -27,7 +27,6 @@ from .value import (
     MultiValuedValue,
     NewTypeValue,
     ReferencingValue,
-    SequenceIncompleteValue,
     SequenceValue,
     TypedValue,
     TypeVarValue,
@@ -41,7 +40,13 @@ from .value import (
     TypedDictValue,
     make_weak,
 )
-from .tests import proxied_fn, autogenerated, l0cached_async_fn, PropertyObject
+from .tests import (
+    proxied_fn,
+    autogenerated,
+    l0cached_async_fn,
+    PropertyObject,
+    make_simple_sequence,
+)
 from . import test_node_visitor
 from .test_node_visitor import assert_passes, assert_fails, only_before, skip_before
 
@@ -132,7 +137,6 @@ def _make_module(code_str: str) -> types.ModuleType:
         MultiValuedValue=MultiValuedValue,
         AnnotatedValue=AnnotatedValue,
         SequenceValue=SequenceValue,
-        SequenceIncompleteValue=SequenceIncompleteValue,
         TypedValue=TypedValue,
         UnboundMethodValue=UnboundMethodValue,
         AnySource=AnySource,
@@ -146,6 +150,7 @@ def _make_module(code_str: str) -> types.ModuleType:
         TypeVarValue=TypeVarValue,
         dump_value=dump_value,
         make_weak=make_weak,
+        make_simple_sequence=make_simple_sequence,
         UNINITIALIZED_VALUE=UNINITIALIZED_VALUE,
         NO_RETURN_VALUE=NO_RETURN_VALUE,
     )
@@ -478,16 +483,14 @@ class TestNameCheckVisitor(TestNameCheckVisitorBase):
 
         def capybara(a, b):
             x = [a, b]
-            assert_is_value(
-                x, SequenceIncompleteValue(list, [UNANNOTATED, UNANNOTATED])
-            )
+            assert_is_value(x, make_simple_sequence(list, [UNANNOTATED, UNANNOTATED]))
             y = a, 2
             assert_is_value(
-                y, SequenceIncompleteValue(tuple, [UNANNOTATED, KnownValue(2)])
+                y, make_simple_sequence(tuple, [UNANNOTATED, KnownValue(2)])
             )
 
             s = {a, b}
-            assert_is_value(s, SequenceIncompleteValue(set, [UNANNOTATED, UNANNOTATED]))
+            assert_is_value(s, make_simple_sequence(set, [UNANNOTATED, UNANNOTATED]))
             z = {a: b}
             assert_is_value(
                 z, DictIncompleteValue(dict, [KVPair(UNANNOTATED, UNANNOTATED)])
@@ -1093,7 +1096,7 @@ class TestIterationTarget(TestNameCheckVisitorBase):
             lst = [1, 2, int(x)]
             assert_is_value(
                 lst,
-                SequenceIncompleteValue(
+                make_simple_sequence(
                     list, [KnownValue(1), KnownValue(2), TypedValue(int)]
                 ),
             )
@@ -1117,7 +1120,7 @@ class TestIterationTarget(TestNameCheckVisitorBase):
 
             lst2 = [x for x in (1, 2)]
             assert_is_value(
-                lst2, SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)])
+                lst2, make_simple_sequence(list, [KnownValue(1), KnownValue(2)])
             )
 
             lst3 = [i + j * 10 for i in range(2) for j in range(3)]
@@ -1260,7 +1263,7 @@ class TestUnboundMethodValue(TestNameCheckVisitorBase):
                 UnboundMethodValue(
                     "append",
                     Composite(
-                        SequenceIncompleteValue(list, [AnyValue(AnySource.unannotated)])
+                        make_simple_sequence(list, [AnyValue(AnySource.unannotated)])
                     ),
                 ),
             )
@@ -1545,10 +1548,12 @@ class TestUnpacking(TestNameCheckVisitorBase):
             degu = (1, *x)
             assert_is_value(
                 degu,
-                make_weak(
-                    GenericValue(
-                        tuple, [KnownValue(1) | AnyValue(AnySource.generic_argument)]
-                    )
+                SequenceValue(
+                    tuple,
+                    [
+                        (False, KnownValue(1)),
+                        (True, AnyValue(AnySource.generic_argument)),
+                    ],
                 ),
             )
 
@@ -1613,7 +1618,7 @@ class TestUnpacking(TestNameCheckVisitorBase):
             *i, j, k = long_tuple
             assert_is_value(
                 i,
-                SequenceIncompleteValue(
+                make_simple_sequence(
                     list, [KnownValue(1), KnownValue(2), KnownValue(3), KnownValue(4)]
                 ),
             )
@@ -1623,7 +1628,7 @@ class TestUnpacking(TestNameCheckVisitorBase):
             assert_is_value(l, KnownValue(1))
             assert_is_value(m, KnownValue(2))
             assert_is_value(
-                n, SequenceIncompleteValue(list, [KnownValue(3), KnownValue(4)])
+                n, make_simple_sequence(list, [KnownValue(3), KnownValue(4)])
             )
             assert_is_value(o, KnownValue(5))
             assert_is_value(p, KnownValue(6))
@@ -1631,7 +1636,7 @@ class TestUnpacking(TestNameCheckVisitorBase):
             q, r, *s = (1, 2)
             assert_is_value(q, KnownValue(1))
             assert_is_value(r, KnownValue(2))
-            assert_is_value(s, SequenceIncompleteValue(list, []))
+            assert_is_value(s, SequenceValue(list, []))
 
             for sprime in []:
                 assert_is_value(sprime, NO_RETURN_VALUE)
diff --git a/pyanalyze/test_patma.py b/pyanalyze/test_patma.py
index a1c94f2..d6ed663 100644
--- a/pyanalyze/test_patma.py
+++ b/pyanalyze/test_patma.py
@@ -53,7 +53,7 @@ class TestPatma(TestNameCheckVisitorBase):
                     case [1, 2, 3]:
                         assert_is_value(
                             seq,
-                            SequenceIncompleteValue(
+                            make_simple_sequence(
                                 tuple,
                                 [TypedValue(int), TypedValue(int), TypedValue(int)]
                             )
diff --git a/pyanalyze/test_signature.py b/pyanalyze/test_signature.py
index d10dabd..181f2c0 100644
--- a/pyanalyze/test_signature.py
+++ b/pyanalyze/test_signature.py
@@ -1,6 +1,7 @@
 # static analysis: ignore
 from collections.abc import Sequence
 
+
 from .value import (
     AnnotatedValue,
     AnySource,
@@ -8,7 +9,6 @@ from .value import (
     CanAssignError,
     GenericValue,
     KnownValue,
-    SequenceIncompleteValue,
     TypedDictValue,
     TypedValue,
     make_weak,
@@ -25,6 +25,7 @@ from .signature import (
     ParameterKind as K,
 )
 from .test_value import CTX
+from .tests import make_simple_sequence
 
 TupleInt = GenericValue(tuple, [TypedValue(int)])
 TupleBool = GenericValue(tuple, [TypedValue(bool)])
@@ -292,7 +293,7 @@ class TestCanAssign:
         )
         object_int = P(
             "args",
-            annotation=SequenceIncompleteValue(
+            annotation=make_simple_sequence(
                 tuple, [TypedValue(object), TypedValue(int)]
             ),
             kind=K.VAR_POSITIONAL,
diff --git a/pyanalyze/test_typeshed.py b/pyanalyze/test_typeshed.py
index 7daa37b..67a801a 100644
--- a/pyanalyze/test_typeshed.py
+++ b/pyanalyze/test_typeshed.py
@@ -23,6 +23,7 @@ from .test_name_check_visitor import TestNameCheckVisitorBase
 from .test_node_visitor import assert_passes
 from .signature import OverloadedSignature, SigParameter, Signature
 from .test_arg_spec import ClassWithCall
+from .tests import make_simple_sequence
 from .typeshed import TypeshedFinder
 from .value import (
     CallableValue,
@@ -38,7 +39,6 @@ from .value import (
     make_weak,
     TypeVarValue,
     UNINITIALIZED_VALUE,
-    SequenceIncompleteValue,
     Value,
 )
 
@@ -386,7 +386,7 @@ class TestGetGenericBases:
     def test_dict_items(self):
         TInt = TypedValue(int)
         TStr = TypedValue(str)
-        TTuple = SequenceIncompleteValue(tuple, [TInt, TStr])
+        TTuple = make_simple_sequence(tuple, [TInt, TStr])
         self.check(
             {
                 "_collections_abc.dict_items": [TInt, TStr],
@@ -445,7 +445,7 @@ class TestGetGenericBases:
     def test_collections(self):
         int_tv = TypedValue(int)
         str_tv = TypedValue(str)
-        int_str_tuple = SequenceIncompleteValue(tuple, [int_tv, str_tv])
+        int_str_tuple = make_simple_sequence(tuple, [int_tv, str_tv])
         self.check(
             {
                 collections.abc.ValuesView: [int_tv],
diff --git a/pyanalyze/test_value.py b/pyanalyze/test_value.py
index b878400..6107354 100644
--- a/pyanalyze/test_value.py
+++ b/pyanalyze/test_value.py
@@ -30,7 +30,6 @@ from .value import (
     TypedValue,
     MultiValuedValue,
     SubclassValue,
-    SequenceIncompleteValue,
     TypeVarMap,
     concrete_values_from_iterable,
     unite_and_simplify,
@@ -221,22 +220,36 @@ def test_generic_value() -> None:
     )
 
 
-def test_sequence_incomplete_value() -> None:
-    val = value.SequenceIncompleteValue(tuple, [TypedValue(int), TypedValue(str)])
+def test_sequence_value() -> None:
+    val = value.SequenceValue(
+        tuple, [(False, TypedValue(int)), (False, TypedValue(str))]
+    )
     assert_can_assign(val, TypedValue(tuple))
     assert_can_assign(val, GenericValue(tuple, [TypedValue(int) | TypedValue(str)]))
     assert_cannot_assign(val, GenericValue(tuple, [TypedValue(int) | TypedValue(list)]))
 
     assert_can_assign(val, val)
-    assert_cannot_assign(val, value.SequenceIncompleteValue(tuple, [TypedValue(int)]))
+    assert_cannot_assign(val, value.SequenceValue(tuple, [(False, TypedValue(int))]))
     assert_can_assign(
-        val, value.SequenceIncompleteValue(tuple, [TypedValue(bool), TypedValue(str)])
+        val,
+        value.SequenceValue(
+            tuple, [(False, TypedValue(bool)), (False, TypedValue(str))]
+        ),
     )
 
-    assert "tuple[int, str]" == str(val)
-    assert "tuple[int]" == str(value.SequenceIncompleteValue(tuple, [TypedValue(int)]))
-    assert "<list containing [int]>" == str(
-        value.SequenceIncompleteValue(list, [TypedValue(int)])
+    assert str(val) == "tuple[int, str]"
+    assert str(value.SequenceValue(tuple, [(False, TypedValue(int))])) == "tuple[int]"
+    assert (
+        str(
+            value.SequenceValue(
+                tuple, [(False, TypedValue(int)), (True, TypedValue(str))]
+            )
+        )
+        == "tuple[int, *tuple[str, ...]]"
+    )
+    assert (
+        str(value.SequenceValue(list, [(False, TypedValue(int))]))
+        == "<list containing [int]>"
     )
 
 
@@ -503,13 +516,14 @@ def test_io() -> None:
 
 def test_concrete_values_from_iterable() -> None:
     assert isinstance(concrete_values_from_iterable(KnownValue(1), CTX), CanAssignError)
-    assert () == concrete_values_from_iterable(KnownValue(()), CTX)
-    assert (KnownValue(1), KnownValue(2)) == concrete_values_from_iterable(
-        KnownValue((1, 2)), CTX
-    )
-    assert (KnownValue(1), KnownValue(2)) == concrete_values_from_iterable(
-        SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]), CTX
-    )
+    assert concrete_values_from_iterable(KnownValue(()), CTX) == []
+    assert concrete_values_from_iterable(KnownValue((1, 2)), CTX) == [
+        KnownValue(1),
+        KnownValue(2),
+    ]
+    assert concrete_values_from_iterable(
+        tests.make_simple_sequence(list, [KnownValue(1), KnownValue(2)]), CTX
+    ) == [KnownValue(1), KnownValue(2)]
     assert TypedValue(int) == concrete_values_from_iterable(
         GenericValue(list, [TypedValue(int)]), CTX
     )
@@ -519,7 +533,7 @@ def test_concrete_values_from_iterable() -> None:
     ] == concrete_values_from_iterable(
         MultiValuedValue(
             [
-                SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]),
+                tests.make_simple_sequence(list, [KnownValue(1), KnownValue(2)]),
                 KnownValue((3, 4)),
             ]
         ),
@@ -530,7 +544,7 @@ def test_concrete_values_from_iterable() -> None:
     ) == concrete_values_from_iterable(
         MultiValuedValue(
             [
-                SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]),
+                tests.make_simple_sequence(list, [KnownValue(1), KnownValue(2)]),
                 GenericValue(list, [TypedValue(int)]),
             ]
         ),
@@ -541,7 +555,7 @@ def test_concrete_values_from_iterable() -> None:
     ) == concrete_values_from_iterable(
         MultiValuedValue(
             [
-                SequenceIncompleteValue(list, [KnownValue(1), KnownValue(2)]),
+                tests.make_simple_sequence(list, [KnownValue(1), KnownValue(2)]),
                 KnownValue((3,)),
             ]
         ),
diff --git a/pyanalyze/tests.py b/pyanalyze/tests.py
index fafab14..92a4f11 100644
--- a/pyanalyze/tests.py
+++ b/pyanalyze/tests.py
@@ -5,12 +5,12 @@ Functions to be used in test_scope unit tests.
 
 """
 
-from typing import ClassVar, Union, overload, NoReturn
+from typing import ClassVar, Sequence, Union, overload, NoReturn
 from asynq import asynq, async_proxy, AsyncTask, ConstFuture, get_async_fn, result
 from asynq.decorators import AsyncDecorator
 import qcore
 
-from .value import VariableNameValue
+from .value import SequenceValue, VariableNameValue, Value
 
 ASYNQ_METHOD_NAME = "asynq"
 ASYNQ_METHOD_NAMES = ("asynq",)
@@ -235,3 +235,7 @@ def overloaded(*args: str) -> Union[int, str]:
 
 def assert_never(arg: NoReturn) -> NoReturn:
     raise RuntimeError("no way")
+
+
+def make_simple_sequence(typ: type, vals: Sequence[Value]) -> SequenceValue:
+    return SequenceValue(typ, [(False, val) for val in vals])